⭐️ Implement personal drive (#61)
* Implement personal drive * Fix tests * Add my-drive tests * Fixing tests * Fixing tests * Fix tests
This commit is contained in:
@@ -3,9 +3,9 @@
|
|||||||
## Run it in development mode
|
## Run it in development mode
|
||||||
|
|
||||||
1. Launch mongo using `docker run -p 27017:27017 -d mongo`
|
1. Launch mongo using `docker run -p 27017:27017 -d mongo`
|
||||||
2. Launch frontend with `cd twake/frontend/; HTTPS=true yarn dev:start`
|
2. Launch frontend with `cd tdrive/frontend/; yarn dev:start`
|
||||||
3. Launch backend with `cd twake/backend/node/; SEARCH_DRIVER=mongodb DB_DRIVER=mongodb PUBSUB_TYPE=local DB_MONGO_URI=mongodb://localhost:27017 STORAGE_LOCAL_PATH=/[full-path-to-store-documents]/documents NODE_ENV=development yarn dev`
|
3. Launch backend with `cd tdrive/backend/node/; SEARCH_DRIVER=mongodb DB_DRIVER=mongodb PUBSUB_TYPE=local DB_MONGO_URI=mongodb://localhost:27017 STORAGE_LOCAL_PATH=/[full-path-to-store-documents]/documents NODE_ENV=development yarn dev`
|
||||||
4. If you need more parameters, create/edit `twake/backend/node/config/development.json` file
|
4. If you need more parameters, create/edit `tdrive/backend/node/config/development.json` file
|
||||||
|
|
||||||
App will be running on port 3000.
|
App will be running on port 3000.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,23 @@ export default {
|
|||||||
added: entity.added,
|
added: entity.added,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
company_id: entity.company_id,
|
company_id: entity.company_id,
|
||||||
|
access_users: ["user_1234", "user_454"],
|
||||||
|
access_users_x_initiator: ["user_1234_x_user_4343", "user_1234_x_user_4343"],
|
||||||
|
|
||||||
|
access_entities: [
|
||||||
|
{
|
||||||
|
type: "user",
|
||||||
|
level: "read",
|
||||||
|
target: "user_1234",
|
||||||
|
source: "user_5678",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "user",
|
||||||
|
level: "read",
|
||||||
|
target: "user_abcd",
|
||||||
|
source: "user_5678",
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
mongoMapping: {
|
mongoMapping: {
|
||||||
text: {
|
text: {
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { logger } from "../../../core/platform/framework";
|
||||||
|
import Repository from "../../../core/platform/services/database/services/orm/repository/repository";
|
||||||
|
import globalResolver from "../../global-resolver";
|
||||||
|
import { DriveFile } from "../entities/drive-file";
|
||||||
|
import { CompanyExecutionContext, DriveFileAccessLevel } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random sha1 access token
|
||||||
|
*
|
||||||
|
* @returns {String} - the random access token ( sha1 hex digest ).
|
||||||
|
*/
|
||||||
|
export const generateAccessToken = (): string => {
|
||||||
|
const randomBytes = crypto.randomBytes(64);
|
||||||
|
|
||||||
|
return crypto.createHash("sha1").update(randomBytes).digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the level meets the required level.
|
||||||
|
*
|
||||||
|
* @param {publicAccessLevel | DriveFileAccessLevel} requiredLevel
|
||||||
|
* @param {publicAccessLevel} level
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const hasAccessLevel = (
|
||||||
|
requiredLevel: DriveFileAccessLevel | "none",
|
||||||
|
level: DriveFileAccessLevel | "none",
|
||||||
|
): boolean => {
|
||||||
|
if (requiredLevel === level) return true;
|
||||||
|
|
||||||
|
if (requiredLevel === "write") {
|
||||||
|
return level === "manage";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredLevel === "read") {
|
||||||
|
return level === "manage" || level === "write";
|
||||||
|
}
|
||||||
|
|
||||||
|
return requiredLevel === "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks the current user is a guest
|
||||||
|
*
|
||||||
|
* @param {CompanyExecutionContext} context
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export const isCompanyGuest = async (context: CompanyExecutionContext): Promise<boolean> => {
|
||||||
|
if (await isCompanyApplication(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRole = await globalResolver.services.companies.getUserRole(
|
||||||
|
context.company.id,
|
||||||
|
context.user?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return userRole === "guest" || !userRole;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks the current user is a admin
|
||||||
|
*
|
||||||
|
* @param {CompanyExecutionContext} context
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export const isCompanyAdmin = async (context: CompanyExecutionContext): Promise<boolean> => {
|
||||||
|
if (await isCompanyApplication(context)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRole = await globalResolver.services.companies.getUserRole(
|
||||||
|
context.company.id,
|
||||||
|
context.user?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return userRole === "admin";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks the current user is a admin
|
||||||
|
*
|
||||||
|
* @param {CompanyExecutionContext} context
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export const isCompanyApplication = async (context: CompanyExecutionContext): Promise<boolean> => {
|
||||||
|
if (context.user?.application_id) {
|
||||||
|
//Applications do everything (if they are added to the company)
|
||||||
|
if (
|
||||||
|
!!(
|
||||||
|
await globalResolver.services.applications.companyApps.get({
|
||||||
|
company_id: context.company.id,
|
||||||
|
application_id: context.user?.application_id,
|
||||||
|
})
|
||||||
|
)?.application?.id
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if access can be granted for the drive item
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {DriveFile | null} item
|
||||||
|
* @param {DriveFileAccessLevel} level
|
||||||
|
* @param {Repository<DriveFile>} repository
|
||||||
|
* @param {CompanyExecutionContext} context
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export const checkAccess = async (
|
||||||
|
id: string,
|
||||||
|
item: DriveFile | null,
|
||||||
|
level: DriveFileAccessLevel,
|
||||||
|
repository: Repository<DriveFile>,
|
||||||
|
context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string },
|
||||||
|
): Promise<boolean> => {
|
||||||
|
if (context.user?.server_request) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const grantedLevel = await getAccessLevel(id, item, repository, context);
|
||||||
|
const hasAccess = hasAccessLevel(level, grantedLevel);
|
||||||
|
logger.info(
|
||||||
|
`Got level ${grantedLevel} for drive item ${id} and required ${level} - returning ${hasAccess}`,
|
||||||
|
);
|
||||||
|
return hasAccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get maximum level for the drive item
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {DriveFile | null} item
|
||||||
|
* @param {Repository<DriveFile>} repository
|
||||||
|
* @param {CompanyExecutionContext} context
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export const getAccessLevel = async (
|
||||||
|
id: string,
|
||||||
|
item: DriveFile | null,
|
||||||
|
repository: Repository<DriveFile>,
|
||||||
|
context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string },
|
||||||
|
): Promise<DriveFileAccessLevel | "none"> => {
|
||||||
|
if (!id || id === "root")
|
||||||
|
return !context?.user?.id ? "none" : (await isCompanyGuest(context)) ? "read" : "manage";
|
||||||
|
if (id === "trash")
|
||||||
|
return (await isCompanyGuest(context)) || !context?.user?.id
|
||||||
|
? "none"
|
||||||
|
: (await isCompanyAdmin(context))
|
||||||
|
? "manage"
|
||||||
|
: "write";
|
||||||
|
|
||||||
|
//If it is my personal folder, I have full access
|
||||||
|
if (context?.user?.id && id.startsWith("user_")) {
|
||||||
|
if (id === "user_" + context.user?.id) return "manage";
|
||||||
|
if (await isCompanyApplication(context)) return "manage";
|
||||||
|
}
|
||||||
|
|
||||||
|
let publicToken = context.public_token;
|
||||||
|
|
||||||
|
try {
|
||||||
|
item =
|
||||||
|
item ||
|
||||||
|
(await repository.findOne({
|
||||||
|
id,
|
||||||
|
company_id: context.company.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
throw Error("Drive item doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await isCompanyApplication(context)) {
|
||||||
|
return "manage";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Specific user or channel rule is applied first. Then less restrictive level will be chosen
|
||||||
|
* between the parent folder and company accesses.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//Public access
|
||||||
|
if (publicToken) {
|
||||||
|
if (!item.access_info.public.token) return "none";
|
||||||
|
const { token: itemToken, level: itemLevel, password, expiration } = item.access_info.public;
|
||||||
|
if (expiration && expiration < Date.now()) return "none";
|
||||||
|
if (password) {
|
||||||
|
const data = publicToken.split("+");
|
||||||
|
if (data.length !== 2) return "none";
|
||||||
|
const [extractedPublicToken, publicTokenPassword] = data;
|
||||||
|
if (publicTokenPassword !== password) return "none";
|
||||||
|
publicToken = extractedPublicToken;
|
||||||
|
}
|
||||||
|
if (itemToken === publicToken) return itemLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessEntities = item.access_info.entities || [];
|
||||||
|
const otherLevels = [];
|
||||||
|
|
||||||
|
//From there a user must be logged in
|
||||||
|
if (context?.user?.id) {
|
||||||
|
//Users
|
||||||
|
const matchingUser = accessEntities.find(a => a.type === "user" && a.id === context.user?.id);
|
||||||
|
if (matchingUser) return matchingUser.level;
|
||||||
|
|
||||||
|
//Channels
|
||||||
|
if (context.tdrive_tab_token) {
|
||||||
|
try {
|
||||||
|
const [channelId] = context.tdrive_tab_token.split("+"); //First item will be the channel id
|
||||||
|
const matchingChannel = accessEntities.find(
|
||||||
|
a => a.type === "channel" && a.id === channelId,
|
||||||
|
);
|
||||||
|
if (matchingChannel) return matchingChannel.level;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Companies
|
||||||
|
const matchingCompany = accessEntities.find(
|
||||||
|
a => a.type === "company" && a.id === context.company.id,
|
||||||
|
);
|
||||||
|
if (matchingCompany) otherLevels.push(matchingCompany.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Parent folder
|
||||||
|
const maxParentFolderLevel =
|
||||||
|
accessEntities.find(a => a.type === "folder" && a.id === "parent")?.level || "none";
|
||||||
|
if (maxParentFolderLevel === "none") {
|
||||||
|
otherLevels.push(maxParentFolderLevel);
|
||||||
|
} else {
|
||||||
|
const parentFolderLevel = await getAccessLevel(item.parent_id, null, repository, context);
|
||||||
|
otherLevels.push(parentFolderLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Return least restrictive level of otherLevels
|
||||||
|
return otherLevels.reduce(
|
||||||
|
(previousValue, b) =>
|
||||||
|
hasAccessLevel(b as DriveFileAccessLevel, previousValue as DriveFileAccessLevel)
|
||||||
|
? previousValue
|
||||||
|
: b,
|
||||||
|
"none",
|
||||||
|
) as DriveFileAccessLevel | "none";
|
||||||
|
} catch (error) {
|
||||||
|
throw Error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Isolate access level information from parent folder logic
|
||||||
|
* Used when putting folder in the trash
|
||||||
|
* @param id
|
||||||
|
* @param item
|
||||||
|
* @param repository
|
||||||
|
*/
|
||||||
|
export const makeStandaloneAccessLevel = async (
|
||||||
|
companyId: string,
|
||||||
|
id: string,
|
||||||
|
item: DriveFile | null,
|
||||||
|
repository: Repository<DriveFile>,
|
||||||
|
options: { removePublicAccess?: boolean } = { removePublicAccess: true },
|
||||||
|
): Promise<DriveFile["access_info"]> => {
|
||||||
|
item =
|
||||||
|
item ||
|
||||||
|
(await repository.findOne({
|
||||||
|
id,
|
||||||
|
company_id: companyId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
throw Error("Drive item doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessInfo = _.cloneDeep(item.access_info);
|
||||||
|
|
||||||
|
if (options?.removePublicAccess && accessInfo?.public?.level) accessInfo.public.level = "none";
|
||||||
|
|
||||||
|
const parentFolderAccess = accessInfo.entities.find(
|
||||||
|
a => a.type === "folder" && a.id === "parent",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!parentFolderAccess || parentFolderAccess.level === "none") {
|
||||||
|
return accessInfo;
|
||||||
|
} else if (item.parent_id !== "root" && item.parent_id !== "trash") {
|
||||||
|
// Get limitations from parent folder
|
||||||
|
const accessEntitiesFromParent = await makeStandaloneAccessLevel(
|
||||||
|
companyId,
|
||||||
|
item.parent_id,
|
||||||
|
null,
|
||||||
|
repository,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mostRestrictiveFolderLevel = parentFolderAccess.level as DriveFileAccessLevel | "none";
|
||||||
|
|
||||||
|
const keptEntities = accessEntitiesFromParent.entities.filter(a => {
|
||||||
|
if (["user", "channel"].includes(a.type)) {
|
||||||
|
return !accessInfo.entities.find(b => b.type === a.type && b.id === a.id);
|
||||||
|
} else {
|
||||||
|
if (a.type === "folder" && a.id === "parent") {
|
||||||
|
mostRestrictiveFolderLevel = hasAccessLevel(a.level, mostRestrictiveFolderLevel)
|
||||||
|
? a.level
|
||||||
|
: mostRestrictiveFolderLevel;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
accessInfo.entities = accessInfo.entities.map(a => {
|
||||||
|
if (a.type === "folder" && a.id === "parent") {
|
||||||
|
a.level = mostRestrictiveFolderLevel;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}) as DriveFile["access_info"]["entities"];
|
||||||
|
|
||||||
|
accessInfo.entities = [...accessInfo.entities, ...keptEntities];
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessInfo;
|
||||||
|
};
|
||||||
@@ -27,17 +27,21 @@ import {
|
|||||||
addDriveItemToArchive,
|
addDriveItemToArchive,
|
||||||
calculateItemSize,
|
calculateItemSize,
|
||||||
canMoveItem,
|
canMoveItem,
|
||||||
checkAccess,
|
|
||||||
getAccessLevel,
|
|
||||||
getDefaultDriveItem,
|
getDefaultDriveItem,
|
||||||
getDefaultDriveItemVersion,
|
getDefaultDriveItemVersion,
|
||||||
getFileMetadata,
|
getFileMetadata,
|
||||||
getItemName,
|
getItemName,
|
||||||
getPath,
|
getPath,
|
||||||
hasAccessLevel,
|
getVirtualFoldersNames,
|
||||||
makeStandaloneAccessLevel,
|
isVirtualFolder,
|
||||||
updateItemSize,
|
updateItemSize,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
import {
|
||||||
|
checkAccess,
|
||||||
|
getAccessLevel,
|
||||||
|
hasAccessLevel,
|
||||||
|
makeStandaloneAccessLevel,
|
||||||
|
} from "./access-check";
|
||||||
import { websocketEventBus } from "../../../core/platform/services/realtime/bus";
|
import { websocketEventBus } from "../../../core/platform/services/realtime/bus";
|
||||||
|
|
||||||
import archiver from "archiver";
|
import archiver from "archiver";
|
||||||
@@ -96,19 +100,18 @@ export class DocumentsService {
|
|||||||
id = id || this.ROOT;
|
id = id || this.ROOT;
|
||||||
|
|
||||||
//Get requested entity
|
//Get requested entity
|
||||||
const entity =
|
const entity = isVirtualFolder(id)
|
||||||
id === this.ROOT || id === this.TRASH
|
? null
|
||||||
? null
|
: await this.repository.findOne(
|
||||||
: await this.repository.findOne(
|
{
|
||||||
{
|
company_id: context.company.id,
|
||||||
company_id: context.company.id,
|
id,
|
||||||
id,
|
},
|
||||||
},
|
{},
|
||||||
{},
|
context,
|
||||||
context,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
if (!entity && !(id === this.ROOT || id === this.TRASH)) {
|
if (!entity && !isVirtualFolder(id)) {
|
||||||
this.logger.error("Drive item not found");
|
this.logger.error("Drive item not found");
|
||||||
throw new CrudException("Item not found", 404);
|
throw new CrudException("Item not found", 404);
|
||||||
}
|
}
|
||||||
@@ -171,9 +174,9 @@ export class DocumentsService {
|
|||||||
({
|
({
|
||||||
id,
|
id,
|
||||||
parent_id: null,
|
parent_id: null,
|
||||||
name: id === this.ROOT ? "root" : id === this.TRASH ? "trash" : "unknown",
|
name: getVirtualFoldersNames(id),
|
||||||
size: await calculateItemSize(
|
size: await calculateItemSize(
|
||||||
id === this.ROOT ? this.ROOT : "trash",
|
{ id, is_directory: true, size: 0 },
|
||||||
this.repository,
|
this.repository,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
import mimes from "../../utils/mime";
|
|
||||||
import { merge } from "lodash";
|
|
||||||
import { DriveFile } from "./entities/drive-file";
|
|
||||||
import {
|
|
||||||
CompanyExecutionContext,
|
|
||||||
DriveExecutionContext,
|
|
||||||
DriveFileAccessLevel,
|
|
||||||
RootType,
|
|
||||||
TrashType,
|
|
||||||
} from "./types";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import { FileVersion, DriveFileMetadata } from "./entities/file-version";
|
|
||||||
import globalResolver from "../global-resolver";
|
|
||||||
import Repository from "../../core/platform/services/database/services/orm/repository/repository";
|
|
||||||
import archiver from "archiver";
|
import archiver from "archiver";
|
||||||
|
import { merge } from "lodash";
|
||||||
|
import PdfParse from "pdf-parse";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
import { stopWords } from "./const";
|
|
||||||
import unoconv from "unoconv-promise";
|
import unoconv from "unoconv-promise";
|
||||||
|
import Repository from "../../core/platform/services/database/services/orm/repository/repository";
|
||||||
import {
|
import {
|
||||||
writeToTemporaryFile,
|
|
||||||
cleanFiles,
|
cleanFiles,
|
||||||
getTmpFile,
|
getTmpFile,
|
||||||
readFromTemporaryFile,
|
|
||||||
readableToBuffer,
|
readableToBuffer,
|
||||||
|
readFromTemporaryFile,
|
||||||
|
writeToTemporaryFile,
|
||||||
} from "../../utils/files";
|
} from "../../utils/files";
|
||||||
import PdfParse from "pdf-parse";
|
import mimes from "../../utils/mime";
|
||||||
import _ from "lodash";
|
import globalResolver from "../global-resolver";
|
||||||
import { logger } from "../../core/platform/framework";
|
import { stopWords } from "./const";
|
||||||
|
import { DriveFile } from "./entities/drive-file";
|
||||||
|
import { DriveFileMetadata, FileVersion } from "./entities/file-version";
|
||||||
|
import { checkAccess, generateAccessToken } from "./services/access-check";
|
||||||
|
import { CompanyExecutionContext, DriveExecutionContext, RootType, TrashType } from "./types";
|
||||||
|
|
||||||
|
const ROOT: RootType = "root";
|
||||||
|
const TRASH: TrashType = "trash";
|
||||||
|
|
||||||
|
export const isVirtualFolder = (id: string) => {
|
||||||
|
return id === ROOT || id === TRASH || id.startsWith("user_");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getVirtualFoldersNames = (id: string) => {
|
||||||
|
if (id.startsWith("user_")) {
|
||||||
|
return "My Drive";
|
||||||
|
}
|
||||||
|
|
||||||
|
return id === ROOT ? "Home" : id === TRASH ? "Trash" : "Unknown";
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default DriveFile object using existing data
|
* Returns the default DriveFile object using existing data
|
||||||
@@ -120,99 +127,6 @@ export const getDefaultDriveItemVersion = (
|
|||||||
return defaultVersion;
|
return defaultVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a random sha1 access token
|
|
||||||
*
|
|
||||||
* @returns {String} - the random access token ( sha1 hex digest ).
|
|
||||||
*/
|
|
||||||
export const generateAccessToken = (): string => {
|
|
||||||
const randomBytes = crypto.randomBytes(64);
|
|
||||||
|
|
||||||
return crypto.createHash("sha1").update(randomBytes).digest("hex");
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the level meets the required level.
|
|
||||||
*
|
|
||||||
* @param {publicAccessLevel | DriveFileAccessLevel} requiredLevel
|
|
||||||
* @param {publicAccessLevel} level
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
export const hasAccessLevel = (
|
|
||||||
requiredLevel: DriveFileAccessLevel | "none",
|
|
||||||
level: DriveFileAccessLevel | "none",
|
|
||||||
): boolean => {
|
|
||||||
if (requiredLevel === level) return true;
|
|
||||||
|
|
||||||
if (requiredLevel === "write") {
|
|
||||||
return level === "manage";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiredLevel === "read") {
|
|
||||||
return level === "manage" || level === "write";
|
|
||||||
}
|
|
||||||
|
|
||||||
return requiredLevel === "none";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* checks the current user is a guest
|
|
||||||
*
|
|
||||||
* @param {CompanyExecutionContext} context
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
export const isCompanyGuest = async (context: CompanyExecutionContext): Promise<boolean> => {
|
|
||||||
if (context.user?.application_id) {
|
|
||||||
//Applications do everything (if they are added to the company)
|
|
||||||
if (
|
|
||||||
!!(
|
|
||||||
await globalResolver.services.applications.companyApps.get({
|
|
||||||
company_id: context.company.id,
|
|
||||||
application_id: context.user?.application_id,
|
|
||||||
})
|
|
||||||
)?.application?.id
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = await globalResolver.services.companies.getUserRole(
|
|
||||||
context.company.id,
|
|
||||||
context.user?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return userRole === "guest" || !userRole;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* checks the current user is a admin
|
|
||||||
*
|
|
||||||
* @param {CompanyExecutionContext} context
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
export const isCompanyAdmin = async (context: CompanyExecutionContext): Promise<boolean> => {
|
|
||||||
if (context.user?.application_id) {
|
|
||||||
//Applications do everything (if they are added to the company)
|
|
||||||
if (
|
|
||||||
!!(
|
|
||||||
await globalResolver.services.applications.companyApps.get({
|
|
||||||
company_id: context.company.id,
|
|
||||||
application_id: context.user?.application_id,
|
|
||||||
})
|
|
||||||
)?.application?.id
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = await globalResolver.services.companies.getUserRole(
|
|
||||||
context.company.id,
|
|
||||||
context.user?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return userRole === "admin";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the size of the Drive Item
|
* Calculates the size of the Drive Item
|
||||||
*
|
*
|
||||||
@@ -222,11 +136,11 @@ export const isCompanyAdmin = async (context: CompanyExecutionContext): Promise<
|
|||||||
* @returns {Promise<number>} - the size of the Drive Item
|
* @returns {Promise<number>} - the size of the Drive Item
|
||||||
*/
|
*/
|
||||||
export const calculateItemSize = async (
|
export const calculateItemSize = async (
|
||||||
item: DriveFile | TrashType | RootType,
|
item: DriveFile | { id: string; is_directory: boolean; size: number },
|
||||||
repository: Repository<DriveFile>,
|
repository: Repository<DriveFile>,
|
||||||
context: CompanyExecutionContext,
|
context: CompanyExecutionContext,
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
if (item === "trash") {
|
if (item.id === "trash") {
|
||||||
const trashedItems = await repository.find(
|
const trashedItems = await repository.find(
|
||||||
{ company_id: context.company.id, parent_id: "trash" },
|
{ company_id: context.company.id, parent_id: "trash" },
|
||||||
{},
|
{},
|
||||||
@@ -236,9 +150,9 @@ export const calculateItemSize = async (
|
|||||||
return trashedItems.getEntities().reduce((acc, curr) => acc + curr.size, 0);
|
return trashedItems.getEntities().reduce((acc, curr) => acc + curr.size, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item === "root" || !item) {
|
if (isVirtualFolder(item.id) || !item) {
|
||||||
const rootFolderItems = await repository.find(
|
const rootFolderItems = await repository.find(
|
||||||
{ company_id: context.company.id, parent_id: "root" },
|
{ company_id: context.company.id, parent_id: item.id || "root" },
|
||||||
{},
|
{},
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
@@ -275,7 +189,7 @@ export const updateItemSize = async (
|
|||||||
repository: Repository<DriveFile>,
|
repository: Repository<DriveFile>,
|
||||||
context: CompanyExecutionContext,
|
context: CompanyExecutionContext,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (!id || id === "root" || id === "trash") return;
|
if (!id || isVirtualFolder(id)) return;
|
||||||
|
|
||||||
const item = await repository.findOne({ id, company_id: context.company.id });
|
const item = await repository.findOne({ id, company_id: context.company.id });
|
||||||
|
|
||||||
@@ -287,7 +201,7 @@ export const updateItemSize = async (
|
|||||||
|
|
||||||
await repository.save(item);
|
await repository.save(item);
|
||||||
|
|
||||||
if (item.parent_id === "root" || item.parent_id === "trash") {
|
if (isVirtualFolder(item.parent_id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,12 +224,12 @@ export const getPath = async (
|
|||||||
context?: DriveExecutionContext,
|
context?: DriveExecutionContext,
|
||||||
): Promise<DriveFile[]> => {
|
): Promise<DriveFile[]> => {
|
||||||
id = id || "root";
|
id = id || "root";
|
||||||
if (id === "root" || id === "trash")
|
if (isVirtualFolder(id))
|
||||||
return !context.public_token || ignoreAccess
|
return !context.public_token || ignoreAccess
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
name: id === "root" ? "Home" : "Trash",
|
name: getVirtualFoldersNames(id),
|
||||||
} as DriveFile,
|
} as DriveFile,
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
@@ -332,234 +246,6 @@ export const getPath = async (
|
|||||||
return [...(await getPath(item.parent_id, repository, ignoreAccess, context)), item];
|
return [...(await getPath(item.parent_id, repository, ignoreAccess, context)), item];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* checks if access can be granted for the drive item
|
|
||||||
*
|
|
||||||
* @param {string} id
|
|
||||||
* @param {DriveFile | null} item
|
|
||||||
* @param {DriveFileAccessLevel} level
|
|
||||||
* @param {Repository<DriveFile>} repository
|
|
||||||
* @param {CompanyExecutionContext} context
|
|
||||||
* @param {string} token
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
export const checkAccess = async (
|
|
||||||
id: string,
|
|
||||||
item: DriveFile | null,
|
|
||||||
level: DriveFileAccessLevel,
|
|
||||||
repository: Repository<DriveFile>,
|
|
||||||
context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string },
|
|
||||||
): Promise<boolean> => {
|
|
||||||
if (context.user?.server_request) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const grantedLevel = await getAccessLevel(id, item, repository, context);
|
|
||||||
const hasAccess = hasAccessLevel(level, grantedLevel);
|
|
||||||
logger.info(
|
|
||||||
`Got level ${grantedLevel} for drive item ${id} and required ${level} - returning ${hasAccess}`,
|
|
||||||
);
|
|
||||||
return hasAccess;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get maximum level for the drive item
|
|
||||||
*
|
|
||||||
* @param {string} id
|
|
||||||
* @param {DriveFile | null} item
|
|
||||||
* @param {Repository<DriveFile>} repository
|
|
||||||
* @param {CompanyExecutionContext} context
|
|
||||||
* @param {string} token
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
export const getAccessLevel = async (
|
|
||||||
id: string,
|
|
||||||
item: DriveFile | null,
|
|
||||||
repository: Repository<DriveFile>,
|
|
||||||
context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string },
|
|
||||||
): Promise<DriveFileAccessLevel | "none"> => {
|
|
||||||
if (!id || id === "root")
|
|
||||||
return !context?.user?.id ? "none" : (await isCompanyGuest(context)) ? "read" : "manage";
|
|
||||||
if (id === "trash")
|
|
||||||
return (await isCompanyGuest(context)) || !context?.user?.id
|
|
||||||
? "none"
|
|
||||||
: (await isCompanyAdmin(context))
|
|
||||||
? "manage"
|
|
||||||
: "write";
|
|
||||||
|
|
||||||
let publicToken = context.public_token;
|
|
||||||
|
|
||||||
try {
|
|
||||||
item =
|
|
||||||
item ||
|
|
||||||
(await repository.findOne({
|
|
||||||
id,
|
|
||||||
company_id: context.company.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
throw Error("Drive item doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.user?.application_id) {
|
|
||||||
//Applications do everything (if they are added to the company)
|
|
||||||
if (
|
|
||||||
!!(
|
|
||||||
await globalResolver.services.applications.companyApps.get({
|
|
||||||
company_id: context.company.id,
|
|
||||||
application_id: context.user?.application_id,
|
|
||||||
})
|
|
||||||
)?.application?.id
|
|
||||||
) {
|
|
||||||
return "manage";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Specific user or channel rule is applied first. Then less restrictive level will be chosen
|
|
||||||
* between the parent folder and company accesses.
|
|
||||||
*/
|
|
||||||
|
|
||||||
//Public access
|
|
||||||
if (publicToken) {
|
|
||||||
if (!item.access_info.public.token) return "none";
|
|
||||||
const { token: itemToken, level: itemLevel, password, expiration } = item.access_info.public;
|
|
||||||
if (expiration && expiration < Date.now()) return "none";
|
|
||||||
if (password) {
|
|
||||||
const data = publicToken.split("+");
|
|
||||||
if (data.length !== 2) return "none";
|
|
||||||
const [extractedPublicToken, publicTokenPassword] = data;
|
|
||||||
if (publicTokenPassword !== password) return "none";
|
|
||||||
publicToken = extractedPublicToken;
|
|
||||||
}
|
|
||||||
if (itemToken === publicToken) return itemLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessEntities = item.access_info.entities || [];
|
|
||||||
const otherLevels = [];
|
|
||||||
|
|
||||||
//From there a user must be logged in
|
|
||||||
if (context?.user?.id) {
|
|
||||||
//Users
|
|
||||||
const matchingUser = accessEntities.find(a => a.type === "user" && a.id === context.user?.id);
|
|
||||||
if (matchingUser) return matchingUser.level;
|
|
||||||
|
|
||||||
//Channels
|
|
||||||
if (context.tdrive_tab_token) {
|
|
||||||
try {
|
|
||||||
const [channelId] = context.tdrive_tab_token.split("+"); //First item will be the channel id
|
|
||||||
const matchingChannel = accessEntities.find(
|
|
||||||
a => a.type === "channel" && a.id === channelId,
|
|
||||||
);
|
|
||||||
if (matchingChannel) return matchingChannel.level;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Companies
|
|
||||||
const matchingCompany = accessEntities.find(
|
|
||||||
a => a.type === "company" && a.id === context.company.id,
|
|
||||||
);
|
|
||||||
if (matchingCompany) otherLevels.push(matchingCompany.level);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Parent folder
|
|
||||||
const maxParentFolderLevel =
|
|
||||||
accessEntities.find(a => a.type === "folder" && a.id === "parent")?.level || "none";
|
|
||||||
if (maxParentFolderLevel === "none") {
|
|
||||||
otherLevels.push(maxParentFolderLevel);
|
|
||||||
} else {
|
|
||||||
const parentFolderLevel = await getAccessLevel(item.parent_id, null, repository, context);
|
|
||||||
otherLevels.push(parentFolderLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return least restrictive level of otherLevels
|
|
||||||
return otherLevels.reduce(
|
|
||||||
(previousValue, b) =>
|
|
||||||
hasAccessLevel(b as DriveFileAccessLevel, previousValue as DriveFileAccessLevel)
|
|
||||||
? previousValue
|
|
||||||
: b,
|
|
||||||
"none",
|
|
||||||
) as DriveFileAccessLevel | "none";
|
|
||||||
} catch (error) {
|
|
||||||
throw Error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Isolate access level information from parent folder logic
|
|
||||||
* Used when putting folder in the trash
|
|
||||||
* @param id
|
|
||||||
* @param item
|
|
||||||
* @param repository
|
|
||||||
*/
|
|
||||||
export const makeStandaloneAccessLevel = async (
|
|
||||||
companyId: string,
|
|
||||||
id: string,
|
|
||||||
item: DriveFile | null,
|
|
||||||
repository: Repository<DriveFile>,
|
|
||||||
options: { removePublicAccess?: boolean } = { removePublicAccess: true },
|
|
||||||
): Promise<DriveFile["access_info"]> => {
|
|
||||||
item =
|
|
||||||
item ||
|
|
||||||
(await repository.findOne({
|
|
||||||
id,
|
|
||||||
company_id: companyId,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
throw Error("Drive item doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessInfo = _.cloneDeep(item.access_info);
|
|
||||||
|
|
||||||
if (options?.removePublicAccess && accessInfo?.public?.level) accessInfo.public.level = "none";
|
|
||||||
|
|
||||||
const parentFolderAccess = accessInfo.entities.find(
|
|
||||||
a => a.type === "folder" && a.id === "parent",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!parentFolderAccess || parentFolderAccess.level === "none") {
|
|
||||||
return accessInfo;
|
|
||||||
} else if (item.parent_id !== "root" && item.parent_id !== "trash") {
|
|
||||||
// Get limitations from parent folder
|
|
||||||
const accessEntitiesFromParent = await makeStandaloneAccessLevel(
|
|
||||||
companyId,
|
|
||||||
item.parent_id,
|
|
||||||
null,
|
|
||||||
repository,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mostRestrictiveFolderLevel = parentFolderAccess.level as DriveFileAccessLevel | "none";
|
|
||||||
|
|
||||||
const keptEntities = accessEntitiesFromParent.entities.filter(a => {
|
|
||||||
if (["user", "channel"].includes(a.type)) {
|
|
||||||
return !accessInfo.entities.find(b => b.type === a.type && b.id === a.id);
|
|
||||||
} else {
|
|
||||||
if (a.type === "folder" && a.id === "parent") {
|
|
||||||
mostRestrictiveFolderLevel = hasAccessLevel(a.level, mostRestrictiveFolderLevel)
|
|
||||||
? a.level
|
|
||||||
: mostRestrictiveFolderLevel;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
accessInfo.entities = accessInfo.entities.map(a => {
|
|
||||||
if (a.type === "folder" && a.id === "parent") {
|
|
||||||
a.level = mostRestrictiveFolderLevel;
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}) as DriveFile["access_info"]["entities"];
|
|
||||||
|
|
||||||
accessInfo.entities = [...accessInfo.entities, ...keptEntities];
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds drive items to an archive recursively
|
* Adds drive items to an archive recursively
|
||||||
*
|
*
|
||||||
@@ -798,23 +484,20 @@ export const canMoveItem = async (
|
|||||||
context: CompanyExecutionContext,
|
context: CompanyExecutionContext,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
if (source === target) return false;
|
if (source === target) return false;
|
||||||
if (target === "root" || target === "trash") return true;
|
|
||||||
|
|
||||||
const item = await repository.findOne({
|
const item = await repository.findOne({
|
||||||
id: source,
|
id: source,
|
||||||
company_id: context.company.id,
|
company_id: context.company.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!item.is_directory) {
|
const targetItem = isVirtualFolder(target)
|
||||||
return true;
|
? null
|
||||||
}
|
: await repository.findOne({
|
||||||
|
id: target,
|
||||||
|
company_id: context.company.id,
|
||||||
|
});
|
||||||
|
|
||||||
const targetItem = await repository.findOne({
|
if (!isVirtualFolder(target) && (!targetItem || !targetItem.is_directory)) {
|
||||||
id: target,
|
|
||||||
company_id: context.company.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!targetItem || !targetItem.is_directory) {
|
|
||||||
throw Error("target item doesn't exist or not a directory");
|
throw Error("target item doesn't exist or not a directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ describe("the Drive feature", () => {
|
|||||||
const response = await e2e_getDocument(platform, "");
|
const response = await e2e_getDocument(platform, "");
|
||||||
const result = deserialize<DriveItemDetailsMockClass>(DriveItemDetailsMockClass, response.body);
|
const result = deserialize<DriveItemDetailsMockClass>(DriveItemDetailsMockClass, response.body);
|
||||||
|
|
||||||
expect(result.item.name).toEqual("root");
|
expect(result.item.id).toEqual("root");
|
||||||
|
expect(result.item.name).toEqual("Home");
|
||||||
|
|
||||||
done?.();
|
done?.();
|
||||||
});
|
});
|
||||||
@@ -111,7 +112,8 @@ describe("the Drive feature", () => {
|
|||||||
const response = await e2e_getDocument(platform, "trash");
|
const response = await e2e_getDocument(platform, "trash");
|
||||||
const result = deserialize<DriveItemDetailsMockClass>(DriveItemDetailsMockClass, response.body);
|
const result = deserialize<DriveItemDetailsMockClass>(DriveItemDetailsMockClass, response.body);
|
||||||
|
|
||||||
expect(result.item.name).toEqual("trash");
|
expect(result.item.id).toEqual("trash");
|
||||||
|
expect(result.item.name).toEqual("Trash");
|
||||||
|
|
||||||
done?.();
|
done?.();
|
||||||
});
|
});
|
||||||
@@ -162,7 +164,7 @@ describe("the Drive feature", () => {
|
|||||||
listTrashResponse.body,
|
listTrashResponse.body,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(listTrashResult.item.name).toEqual("trash");
|
expect(listTrashResult.item.name).toEqual("Trash");
|
||||||
expect(listTrashResult.children.some(({ id }) => id === createItemResult.id)).toBeTruthy();
|
expect(listTrashResult.children.some(({ id }) => id === createItemResult.id)).toBeTruthy();
|
||||||
|
|
||||||
done?.();
|
done?.();
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { describe, beforeEach, afterEach, it, expect, afterAll } from "@jest/globals";
|
||||||
|
import { deserialize } from "class-transformer";
|
||||||
|
import { File } from "../../../src/services/files/entities/file";
|
||||||
|
import { ResourceUpdateResponse } from "../../../src/utils/types";
|
||||||
|
import { init, TestPlatform } from "../setup";
|
||||||
|
import { TestDbService } from "../utils.prepare.db";
|
||||||
|
import {
|
||||||
|
e2e_createDocument,
|
||||||
|
e2e_createDocumentFile,
|
||||||
|
e2e_createVersion,
|
||||||
|
e2e_deleteDocument,
|
||||||
|
e2e_getDocument,
|
||||||
|
e2e_searchDocument,
|
||||||
|
e2e_updateDocument,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
describe("the My Drive feature", () => {
|
||||||
|
let platform: TestPlatform;
|
||||||
|
|
||||||
|
class DriveFileMockClass {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
added: string;
|
||||||
|
parent_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DriveItemDetailsMockClass {
|
||||||
|
path: string[];
|
||||||
|
item: DriveFileMockClass;
|
||||||
|
children: DriveFileMockClass[];
|
||||||
|
versions: Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchResultMockClass {
|
||||||
|
entities: DriveFileMockClass[];
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
platform = await init({
|
||||||
|
services: [
|
||||||
|
"webserver",
|
||||||
|
"database",
|
||||||
|
"applications",
|
||||||
|
"search",
|
||||||
|
"storage",
|
||||||
|
"message-queue",
|
||||||
|
"user",
|
||||||
|
"search",
|
||||||
|
"files",
|
||||||
|
"websocket",
|
||||||
|
"messages",
|
||||||
|
"auth",
|
||||||
|
"realtime",
|
||||||
|
"channels",
|
||||||
|
"counter",
|
||||||
|
"statistics",
|
||||||
|
"platform-services",
|
||||||
|
"documents",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await platform.tearDown();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await platform.app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
const createItem = async (): Promise<DriveFileMockClass> => {
|
||||||
|
await TestDbService.getInstance(platform, true);
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
name: "new test file",
|
||||||
|
parent_id: "user_" + platform.currentUser.id,
|
||||||
|
company_id: platform.workspace.company_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const version = {};
|
||||||
|
|
||||||
|
const response = await e2e_createDocument(platform, item, version);
|
||||||
|
return deserialize<DriveFileMockClass>(DriveFileMockClass, response.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("did create the drive item in my user folder", async done => {
|
||||||
|
const result = await createItem();
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.name).toEqual("new test file");
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
|
||||||
|
done?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("did move an item to root and back", async done => {
|
||||||
|
const createItemResult = await createItem();
|
||||||
|
|
||||||
|
expect(createItemResult.id).toBeDefined();
|
||||||
|
|
||||||
|
let updateItemResponse = await e2e_updateDocument(platform, createItemResult.id, {
|
||||||
|
parent_id: "root",
|
||||||
|
});
|
||||||
|
let updateItemResult = deserialize<DriveFileMockClass>(
|
||||||
|
DriveFileMockClass,
|
||||||
|
updateItemResponse.body,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(createItemResult.id).toEqual(updateItemResult.id);
|
||||||
|
expect(updateItemResult.parent_id).toEqual("root");
|
||||||
|
|
||||||
|
updateItemResponse = await e2e_updateDocument(platform, createItemResult.id, {
|
||||||
|
parent_id: "user_" + platform.currentUser.id,
|
||||||
|
});
|
||||||
|
updateItemResult = deserialize<DriveFileMockClass>(DriveFileMockClass, updateItemResponse.body);
|
||||||
|
|
||||||
|
expect(createItemResult.id).toEqual(updateItemResult.id);
|
||||||
|
expect(updateItemResult.parent_id).toEqual("user_" + platform.currentUser.id);
|
||||||
|
|
||||||
|
done?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can't move an item to another user folder", async done => {
|
||||||
|
const createItemResult = await createItem();
|
||||||
|
|
||||||
|
expect(createItemResult.id).toBeDefined();
|
||||||
|
|
||||||
|
let updateItemResponse = await e2e_updateDocument(platform, createItemResult.id, {
|
||||||
|
parent_id: "user_2123",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateItemResponse.statusCode).not.toBe(200);
|
||||||
|
|
||||||
|
done?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Button } from '@atoms/button/button';
|
import { Button } from '@atoms/button/button';
|
||||||
import { Title } from '@atoms/text';
|
import { Title } from '@atoms/text';
|
||||||
import { DriveItem } from '@features/drive/types';
|
import { DriveItem } from '@features/drive/types';
|
||||||
|
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { PublicIcon } from './components/public-icon';
|
import { PublicIcon } from './components/public-icon';
|
||||||
|
import MenusManager from '@components/menus/menus-manager.jsx';
|
||||||
|
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||||
|
|
||||||
export default ({
|
export default ({
|
||||||
path: livePath,
|
path: livePath,
|
||||||
@@ -56,6 +59,7 @@ const PathItem = ({
|
|||||||
first?: boolean;
|
first?: boolean;
|
||||||
onClick: (id: string) => void;
|
onClick: (id: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { user } = useCurrentUser();
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
theme={last ? 'primary' : 'default'}
|
theme={last ? 'primary' : 'default'}
|
||||||
@@ -65,8 +69,19 @@ const PathItem = ({
|
|||||||
(!last ? 'rounded-r-none ' : '') +
|
(!last ? 'rounded-r-none ' : '') +
|
||||||
(!first && !last ? 'max-w-[15ch] ' : '')
|
(!first && !last ? 'max-w-[15ch] ' : '')
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={evt => {
|
||||||
onClick(item?.id || '');
|
if (first) {
|
||||||
|
MenusManager.openMenu(
|
||||||
|
[
|
||||||
|
{ type: 'menu', text: 'Home', onClick: () => onClick('root') },
|
||||||
|
{ type: 'menu', text: 'My Drive', onClick: () => onClick('user_' + user?.id) },
|
||||||
|
],
|
||||||
|
{ x: evt.clientX, y: evt.clientY },
|
||||||
|
'center',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onClick(item?.id || '');
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-ellipsis overflow-hidden whitespace-nowrap" style={{ maxWidth: 120 }}>
|
<span className="text-ellipsis overflow-hidden whitespace-nowrap" style={{ maxWidth: 120 }}>
|
||||||
@@ -75,6 +90,11 @@ const PathItem = ({
|
|||||||
{item?.access_info?.public?.level && item?.access_info?.public?.level !== 'none' && (
|
{item?.access_info?.public?.level && item?.access_info?.public?.level !== 'none' && (
|
||||||
<PublicIcon className="h-5 w-5 ml-2" />
|
<PublicIcon className="h-5 w-5 ml-2" />
|
||||||
)}
|
)}
|
||||||
|
{first && (
|
||||||
|
<span className="ml-2 -mr-1">
|
||||||
|
<ChevronDownIcon className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
HeartIcon,
|
HeartIcon,
|
||||||
ShareIcon,
|
ShareIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
UserIcon,
|
||||||
} from '@heroicons/react/outline';
|
} from '@heroicons/react/outline';
|
||||||
|
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { Title } from '../../../atoms/text';
|
import { Title } from '../../../atoms/text';
|
||||||
import { useDriveItem } from '../../../features/drive/hooks/use-drive-item';
|
import { useDriveItem } from '../../../features/drive/hooks/use-drive-item';
|
||||||
@@ -18,10 +20,14 @@ import Actions from './actions';
|
|||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [parentId, setParentId] = useRecoilState(DriveCurrentFolderAtom('root'));
|
const [parentId, setParentId] = useRecoilState(DriveCurrentFolderAtom('root'));
|
||||||
|
const { user } = useCurrentUser();
|
||||||
const active = false;
|
const active = false;
|
||||||
const { access: rootAccess } = useDriveItem('root');
|
const { access: rootAccess } = useDriveItem('root');
|
||||||
const { inTrash } = useDriveItem(parentId);
|
const { inTrash, path } = useDriveItem(parentId);
|
||||||
const activeClass = 'bg-zinc-50 dark:bg-zinc-800 !text-blue-500';
|
const activeClass = 'bg-zinc-50 dark:bg-zinc-800 !text-blue-500';
|
||||||
|
let folderType = 'home';
|
||||||
|
if ((path || [])[0]?.id === 'user_' + user?.id) folderType = 'personal';
|
||||||
|
if (inTrash) folderType = 'trash';
|
||||||
return (
|
return (
|
||||||
<div className="grow flex flex-col overflow-auto -m-4 p-4 relative">
|
<div className="grow flex flex-col overflow-auto -m-4 p-4 relative">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
@@ -45,10 +51,18 @@ export default () => {
|
|||||||
onClick={() => setParentId('root')}
|
onClick={() => setParentId('root')}
|
||||||
size="lg"
|
size="lg"
|
||||||
theme="white"
|
theme="white"
|
||||||
className={'w-full mt-2 mb-1 ' + (!inTrash ? activeClass : '')}
|
className={'w-full mt-2 mb-1 ' + (folderType === 'home' ? activeClass : '')}
|
||||||
>
|
>
|
||||||
<CloudIcon className="w-5 h-5 mr-4" /> Home
|
<CloudIcon className="w-5 h-5 mr-4" /> Home
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setParentId('user_' + user?.id)}
|
||||||
|
size="lg"
|
||||||
|
theme="white"
|
||||||
|
className={'w-full mb-1 ' + (folderType === 'personal' ? activeClass : '')}
|
||||||
|
>
|
||||||
|
<UserIcon className="w-5 h-5 mr-4" /> My Drive
|
||||||
|
</Button>
|
||||||
{false && (
|
{false && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@@ -72,7 +86,7 @@ export default () => {
|
|||||||
onClick={() => setParentId('trash')}
|
onClick={() => setParentId('trash')}
|
||||||
size="lg"
|
size="lg"
|
||||||
theme="white"
|
theme="white"
|
||||||
className={'w-full mb-1 ' + (inTrash ? activeClass : '')}
|
className={'w-full mb-1 ' + (folderType === 'trash' ? activeClass : '')}
|
||||||
>
|
>
|
||||||
<TrashIcon className="w-5 h-5 mr-4 text-rose-500" /> Trash
|
<TrashIcon className="w-5 h-5 mr-4 text-rose-500" /> Trash
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user