diff --git a/tdrive/backend/node/config/custom-environment-variables.json b/tdrive/backend/node/config/custom-environment-variables.json index 7e4f7abea..1bf1e9de8 100644 --- a/tdrive/backend/node/config/custom-environment-variables.json +++ b/tdrive/backend/node/config/custom-environment-variables.json @@ -102,5 +102,8 @@ }, "plugins": { "server": "PLUGINS_SERVER" + }, + "drive": { + "rootAdmins": "DRIVE_ROOT_ADMINS" } } diff --git a/tdrive/backend/node/src/core/platform/services/search/adapters/elasticsearch/index.ts b/tdrive/backend/node/src/core/platform/services/search/adapters/elasticsearch/index.ts index 9becaa965..d6458e731 100644 --- a/tdrive/backend/node/src/core/platform/services/search/adapters/elasticsearch/index.ts +++ b/tdrive/backend/node/src/core/platform/services/search/adapters/elasticsearch/index.ts @@ -40,7 +40,7 @@ export default class ElasticSearch extends SearchAdapter implements SearchAdapte public async connect() { try { - let clientOptions: any = { + const clientOptions: any = { node: this.configuration.endpoint, ssl: { rejectUnauthorized: false, @@ -48,7 +48,7 @@ export default class ElasticSearch extends SearchAdapter implements SearchAdapte }; if (this.configuration.useAuth) { - logger.info(`Using auth for ES client`); + logger.info("Using auth for ES client"); clientOptions.auth = { username: this.configuration.username, password: this.configuration.password, diff --git a/tdrive/backend/node/src/services/documents/services/access-check.ts b/tdrive/backend/node/src/services/documents/services/access-check.ts index 40391554c..5c14daabd 100644 --- a/tdrive/backend/node/src/services/documents/services/access-check.ts +++ b/tdrive/backend/node/src/services/documents/services/access-check.ts @@ -61,6 +61,26 @@ export const isCompanyGuest = async (context: CompanyExecutionContext): Promise< return userRole === "guest" || !userRole; }; +/** + * checks the current user is a member + * Company members can access shared drive + * + * @param {CompanyExecutionContext} context + * @returns {Promise} + */ +export const isCompanyMember = async (context: CompanyExecutionContext): Promise => { + if (await isCompanyApplication(context)) { + return false; + } + + const userRole = await globalResolver.services.companies.getUserRole( + context.company.id, + context.user?.id, + ); + + return userRole === "member" || !userRole; +}; + /** * checks the current user is a admin * @@ -149,8 +169,15 @@ export const getAccessLevel = async ( repository: Repository, context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string }, ): Promise => { - if (!id || id === "root") - return !context?.user?.id || (await isCompanyGuest(context)) ? "none" : "manage"; + const isMember = !context?.user?.id || (await isCompanyMember(context)); + if (!id || id === "root") { + if (!context?.user?.id || (await isCompanyGuest(context))) { + return "none"; + } else { + if (isMember) return "read"; + return "manage"; + } + } if (id === "trash") return (await isCompanyGuest(context)) || !context?.user?.id ? "none" @@ -182,6 +209,9 @@ export const getAccessLevel = async ( } if (await isCompanyApplication(context)) { + if (!id.startsWith("user_") && isMember && item.creator != context.user.id) { + return "read"; + } return "manage"; } diff --git a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts index d3e13fdf8..32d1f2438 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -5,7 +5,11 @@ import { CrudException, ListResult } from "../../../../core/platform/framework/a import { File } from "../../../../services/files/entities/file"; import { UploadOptions } from "../../../../services/files/types"; import globalResolver from "../../../../services/global-resolver"; -import { PaginationQueryParameters, ResourceWebsocket } from "../../../../utils/types"; +import { + PaginationQueryParameters, + ResourceWebsocket, + CompanyUserRole, +} from "../../../../utils/types"; import { DriveFile } from "../../entities/drive-file"; import { FileVersion } from "../../entities/file-version"; import { @@ -21,9 +25,21 @@ import { } from "../../types"; import { DriveFileDTO } from "../dto/drive-file-dto"; import { DriveFileDTOBuilder } from "../../services/drive-file-dto-builder"; +import { ExecutionContext } from "../../../../core/platform/framework/api/crud-service"; +import gr from "../../../global-resolver"; +import config from "config"; export class DocumentsController { private driveFileDTOBuilder = new DriveFileDTOBuilder(); + private rootAdmins: string[] = config.has("drive.rootAdmins") + ? config.get("drive.rootAdmins") + : []; + + private getCompanyUserRole(companyId: string, userId: string, context?: ExecutionContext) { + return gr.services.companies + .getCompanyUser({ id: companyId }, { id: userId }, context) + .then(a => (a ? a.level : null)); + } /** * Creates a DriveFile item @@ -166,7 +182,7 @@ export class DocumentsController { onlyUploadedNotByMe: true, }; - return { + const data = { ...(await globalResolver.services.documents.documents.browse(id, options, context)), websockets: request.currentUser?.id ? globalResolver.platformServices.realtime.sign( @@ -175,6 +191,8 @@ export class DocumentsController { ) : [], }; + + return data; }; sharedWithMe = async ( @@ -257,6 +275,45 @@ export class DocumentsController { return await globalResolver.services.documents.documents.update(id, update, context); }; + updateLevel = async ( + request: FastifyRequest<{ + Params: ItemRequestParams; + Body: Partial | any; + Querystring: { public_token?: string }; + }>, + ): Promise => { + const { id } = request.params; + const update = request.body; + + if (!id) throw new CrudException("Missing id", 400); + + if (!this.rootAdmins.includes(request.currentUser.email)) { + throw new CrudException("Unauthorized access. User is not a root admin.", 401); + } + + if (id == "root") { + const companyUser = await globalResolver.services.companies.getCompanyUser( + { id: update.company_id }, + { id: update.user_id }, + ); + if (companyUser) { + let level = CompanyUserRole.member; + if (update.level == "manage") { + level = CompanyUserRole.admin; + } + await globalResolver.services.companies.setUserRole( + update.company_id, + update.user_id, + companyUser.role, + level, + ); + } else { + throw new CrudException("User is not part of this company.", 406); + } + } + return {}; + }; + /** * Create a drive file version. * diff --git a/tdrive/backend/node/src/services/documents/web/routes.ts b/tdrive/backend/node/src/services/documents/web/routes.ts index d07a037d8..fa7890f4c 100644 --- a/tdrive/backend/node/src/services/documents/web/routes.ts +++ b/tdrive/backend/node/src/services/documents/web/routes.ts @@ -37,6 +37,13 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next) handler: documentsController.update.bind(documentsController), }); + fastify.route({ + method: "POST", + url: `${serviceUrl}/:id/level`, + preValidation: [fastify.authenticateOptional], + handler: documentsController.updateLevel.bind(documentsController), + }); + fastify.route({ method: "DELETE", url: `${serviceUrl}/:id`, diff --git a/tdrive/backend/node/src/services/user/services/companies.ts b/tdrive/backend/node/src/services/user/services/companies.ts index 94d2a78e5..585ea2d7c 100644 --- a/tdrive/backend/node/src/services/user/services/companies.ts +++ b/tdrive/backend/node/src/services/user/services/companies.ts @@ -261,6 +261,7 @@ export class CompanyServiceImpl { companyId: uuid, userId: uuid, role: CompanyUserRole = "member", + level?: any, applications: string[] = [], context?: ExecutionContext, ): Promise> { @@ -275,6 +276,9 @@ export class CompanyServiceImpl { } entity.role = role; + if (level) { + entity.level = level; + } entity.applications = applications; await this.companyUserRepository.save(entity, context); diff --git a/tdrive/backend/node/src/services/user/web/controller.ts b/tdrive/backend/node/src/services/user/web/controller.ts index 874354613..d851c300b 100644 --- a/tdrive/backend/node/src/services/user/web/controller.ts +++ b/tdrive/backend/node/src/services/user/web/controller.ts @@ -26,6 +26,7 @@ import { UserListQueryParameters, UserObject, UserParameters, + CompanyUsersParameters, } from "./types"; import Company from "../entities/company"; import CompanyUser from "../entities/company_user"; @@ -144,6 +145,36 @@ export class UsersCrudController }; } + async all( + request: FastifyRequest<{ + Querystring: UserListQueryParameters; + Params: CompanyUsersParameters; + }>, + ): Promise> { + const companyId = request.params.companyId; + + const users = await gr.services.users.search( + new Pagination(request.query.page_token, request.query.limit), + { + search: "", + companyId, + }, + ); + + const resUsers = await Promise.all( + users.getEntities().map(user => + formatUser(user, { + includeCompanies: true, + }), + ), + ); + + // return users; + return { + resources: resUsers, + }; + } + async getUserCompanies( request: FastifyRequest<{ Params: UserParameters }>, _reply: FastifyReply, diff --git a/tdrive/backend/node/src/services/user/web/routes.ts b/tdrive/backend/node/src/services/user/web/routes.ts index f90726121..d7ed218fa 100644 --- a/tdrive/backend/node/src/services/user/web/routes.ts +++ b/tdrive/backend/node/src/services/user/web/routes.ts @@ -60,6 +60,15 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) handler: usersController.list.bind(usersController), }); + fastify.route({ + method: "POST", + url: `${usersUrl}/:companyId/all`, + preHandler: accessControl, + preValidation: [fastify.authenticate], + schema: getUsersSchema, + handler: usersController.all.bind(usersController), + }); + // Get a list of companies for a user, only common companies with current user are returned. fastify.route({ method: "GET", diff --git a/tdrive/backend/node/src/services/user/web/types.ts b/tdrive/backend/node/src/services/user/web/types.ts index d3de93ea8..d9667a050 100644 --- a/tdrive/backend/node/src/services/user/web/types.ts +++ b/tdrive/backend/node/src/services/user/web/types.ts @@ -13,6 +13,11 @@ export interface UserParameters { id: string; } +export interface CompanyUsersParameters { + /* user id */ + companyId: string; +} + export interface CompanyParameters { /* company id */ id: string; diff --git a/tdrive/backend/node/src/utils/types.ts b/tdrive/backend/node/src/utils/types.ts index d4b2f1e7e..bf3c5909d 100644 --- a/tdrive/backend/node/src/utils/types.ts +++ b/tdrive/backend/node/src/utils/types.ts @@ -130,3 +130,8 @@ export interface JWTObject { email: string; track: boolean; } + +export enum CompanyUserRole { + member = 1, + admin = 0, +} diff --git a/tdrive/backend/node/test/e2e/common/common_test_helpers.ts b/tdrive/backend/node/test/e2e/common/common_test_helpers.ts index 9a019ac60..4e27b8e63 100644 --- a/tdrive/backend/node/test/e2e/common/common_test_helpers.ts +++ b/tdrive/backend/node/test/e2e/common/common_test_helpers.ts @@ -39,12 +39,12 @@ export default class TestHelpers { this.platform = platform } - private async init(newUser: boolean) { + private async init(newUser: boolean, options?: {}) { this.dbService = await TestDbService.getInstance(this.platform, true); if (newUser) { this.workspace = this.platform.workspace; const workspacePK = {id: this.workspace.workspace_id, company_id: this.workspace.company_id}; - this.user = await this.dbService.createUser([workspacePK], {}, uuidv1()); + this.user = await this.dbService.createUser([workspacePK], options, uuidv1()); } else { this.user = this.platform.currentUser; this.workspace = this.platform.workspace; @@ -52,9 +52,9 @@ export default class TestHelpers { this.jwt = this.getJWTTokenForUser(this.user.id); } - public static async getInstance(platform: TestPlatform, newUser = false): Promise { + public static async getInstance(platform: TestPlatform, newUser = false, options?: {}): Promise { const helpers = new TestHelpers(platform); - await helpers.init(newUser) + await helpers.init(newUser, options) return helpers; } diff --git a/tdrive/backend/node/test/e2e/documents/documents-browser.spec.ts b/tdrive/backend/node/test/e2e/documents/documents-browser.spec.ts index 9e15227e5..c8cb5aff3 100644 --- a/tdrive/backend/node/test/e2e/documents/documents-browser.spec.ts +++ b/tdrive/backend/node/test/e2e/documents/documents-browser.spec.ts @@ -54,7 +54,7 @@ describe("The Documents Browser Window and API", () => { it("Should not be visible for other users", async () => { const myDriveId = "user_" + currentUser.user.id; - const anotherUser = await TestHelpers.getInstance(platform, true); + const anotherUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); await currentUser.uploadAllFilesOneByOne(myDriveId); await new Promise(r => setTimeout(r, 5000)); @@ -107,8 +107,8 @@ describe("The Documents Browser Window and API", () => { it("Should contain files that were shared with the user", async () => { const sharedWIthMeFolder = "shared_with_me"; - const oneUser = await TestHelpers.getInstance(platform, true); - const anotherUser = await TestHelpers.getInstance(platform, true); + const oneUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); + const anotherUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); let files = await oneUser.uploadAllFilesOneByOne(); await new Promise(r => setTimeout(r, 5000)); diff --git a/tdrive/backend/node/test/e2e/documents/documents.spec.ts b/tdrive/backend/node/test/e2e/documents/documents.spec.ts index 024fa678a..36407fe4b 100644 --- a/tdrive/backend/node/test/e2e/documents/documents.spec.ts +++ b/tdrive/backend/node/test/e2e/documents/documents.spec.ts @@ -147,7 +147,7 @@ describe("the Drive feature", () => { }); it("did search for an item", async () => { - jest.setTimeout(10000); + // jest.setTimeout(10000); const createItemResult = await createItem(); expect(createItemResult.id).toBeDefined(); @@ -171,10 +171,10 @@ describe("the Drive feature", () => { }); it("did search for an item and check that all the fields for 'shared_with_me' view", async () => { - jest.setTimeout(20000); + // jest.setTimeout(20000); //given:: user uploaded one doc and give permission to another user - const oneUser = await TestHelpers.getInstance(platform, true); - const anotherUser = await TestHelpers.getInstance(platform, true); + const oneUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); + const anotherUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); //upload files const doc = await oneUser.uploadRandomFileAndCreateDocument(); await new Promise(r => setTimeout(r, 3000)); @@ -212,10 +212,10 @@ describe("the Drive feature", () => { }); it("'shared_with_me' shouldn't return files uploaded by me", async () => { - jest.setTimeout(20000); + // jest.setTimeout(20000); //given:: user uploaded one doc and give permission to another user - const oneUser = await TestHelpers.getInstance(platform, true); - const anotherUser = await TestHelpers.getInstance(platform, true); + const oneUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); + const anotherUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); const doc = await oneUser.uploadRandomFileAndCreateDocument(); await new Promise(r => setTimeout(r, 3000)); //give permissions to the file @@ -277,7 +277,7 @@ describe("the Drive feature", () => { }); it("did search by mime type", async () => { - jest.setTimeout(10000); + // jest.setTimeout(10000); // given:: all the sample files uploaded and documents for them created await currentUser.uploadAllFilesOneByOne(); @@ -295,8 +295,8 @@ describe("the Drive feature", () => { }); it("did search by last modified", async () => { - jest.setTimeout(10000); - const user = await TestHelpers.getInstance(platform, true); + // jest.setTimeout(10000); + const user = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); // given:: all the sample files uploaded and documents for them created const start = new Date().getTime(); await user.uploadAllFilesOneByOne(); @@ -320,8 +320,8 @@ describe("the Drive feature", () => { it("did search a file shared by another user", async () => { //given: - const oneUser = await TestHelpers.getInstance(platform, true); - const anotherUser = await TestHelpers.getInstance(platform, true); + const oneUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); + const anotherUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); //upload files let files = await oneUser.uploadAllFilesOneByOne(); @@ -348,10 +348,10 @@ describe("the Drive feature", () => { }, 30000000); it("did search a file by file owner", async () => { - jest.setTimeout(30000); + // jest.setTimeout(30000); //given: - const oneUser = await TestHelpers.getInstance(platform, true); - const anotherUser = await TestHelpers.getInstance(platform, true); + const oneUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); + const anotherUser = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); //upload files let files = await oneUser.uploadAllFilesOneByOne(); await anotherUser.uploadAllFilesOneByOne(); @@ -386,8 +386,8 @@ describe("the Drive feature", () => { }); it("did search by 'added' date", async () => { - jest.setTimeout(10000); - const user = await TestHelpers.getInstance(platform, true); + // jest.setTimeout(10000); + const user = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); // given:: all the sample files uploaded and documents for them created await user.uploadRandomFileAndCreateDocument(); const start = new Date().getTime(); @@ -411,7 +411,7 @@ describe("the Drive feature", () => { }); it("did search order by name", async () => { - const user = await TestHelpers.getInstance(platform, true); + const user = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); // given:: all the sample files uploaded and documents for them created await user.uploadAllFilesAndCreateDocuments(); //wait for putting docs to elastic and its indexing @@ -430,8 +430,8 @@ describe("the Drive feature", () => { }, 30000); it("did search order by name desc", async () => { - jest.setTimeout(10000); - const user = await TestHelpers.getInstance(platform, true); + // jest.setTimeout(10000); + const user = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); // given:: all the sample files uploaded and documents for them created await user.uploadAllFilesOneByOne(); //wait for putting docs to elastic and its indexing @@ -450,8 +450,8 @@ describe("the Drive feature", () => { }); it("did search order by added date", async () => { - jest.setTimeout(10000); - const user = await TestHelpers.getInstance(platform, true); + // jest.setTimeout(10000); + const user = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); // given:: all the sample files uploaded and documents for them created await user.uploadAllFilesOneByOne(); //wait for putting docs to elastic and its indexing @@ -470,8 +470,8 @@ describe("the Drive feature", () => { }); it("did search order by added date desc", async () => { - jest.setTimeout(10000); - const user = await TestHelpers.getInstance(platform, true); + // jest.setTimeout(10000); + const user = await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); // given:: all the sample files uploaded and documents for them created await user.uploadAllFilesOneByOne(); //wait for putting docs to elastic and its indexing diff --git a/tdrive/backend/node/test/e2e/documents/public-links.spec.ts b/tdrive/backend/node/test/e2e/documents/public-links.spec.ts index e2c9e186c..7d5d4fcb7 100644 --- a/tdrive/backend/node/test/e2e/documents/public-links.spec.ts +++ b/tdrive/backend/node/test/e2e/documents/public-links.spec.ts @@ -66,7 +66,7 @@ describe("the public links feature", () => { describe("Basic Flow", () => { const createItem = async (): Promise => { - await TestDbService.getInstance(platform, true); + await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); const item = { name: "public file", diff --git a/tdrive/docker-compose.dev.mongo.yml b/tdrive/docker-compose.dev.mongo.yml index 5fcd678c6..1429d2630 100644 --- a/tdrive/docker-compose.dev.mongo.yml +++ b/tdrive/docker-compose.dev.mongo.yml @@ -15,7 +15,7 @@ services: build: context: . dockerfile: docker/tdrive-node/Dockerfile - target: production + target: development container_name: tdrive-node hostname: tdrive_node ports: @@ -28,6 +28,7 @@ services: - PUBSUB_TYPE=local - ./docker-data/documents/:/storage/ volumes: + - ./backend/node/src:/usr/src/app/src - ./docker-data/documents/:/storage/ depends_on: - mongo diff --git a/tdrive/frontend/public/locales/en.json b/tdrive/frontend/public/locales/en.json index d687175bf..077901f64 100644 --- a/tdrive/frontend/public/locales/en.json +++ b/tdrive/frontend/public/locales/en.json @@ -208,6 +208,7 @@ "components.internal-access_cannal_info_give_back":"You will need to go to Tdrive chat to give back access to this item.", "components.internal-access_specific_rules":"Specific access rules", "components.internal-access_specific_rules_you":"(you)", + "components.internal-manage_root_users":"Shared drive access rules", "components.select-users_search_users":"Search users", "common.access-level_full_acess":"Full access", "common.access-level_read":"Read", @@ -306,6 +307,7 @@ "components.item_context_menu.add_documents": "Add document or folder", "components.item_context_menu.download_folder": "Download folder", "components.item_context_menu.go_to_trash": "Go to trash", + "components.item_context_menu.manage_users": "Manager users", "components.item_context_menu.all": "All", "components.item_context_menu.today": "Today", "components.item_context_menu.last_week": "Last week", diff --git a/tdrive/frontend/public/locales/fr.json b/tdrive/frontend/public/locales/fr.json index d11f96b78..4a3a67a29 100644 --- a/tdrive/frontend/public/locales/fr.json +++ b/tdrive/frontend/public/locales/fr.json @@ -192,6 +192,7 @@ "components.internal-access_cannal_info_give_back":"Vous devez aller sur Tdrive chat pour redonner accès à ce fichier", "components.internal-access_specific_rules":"Droit d'accès spécifique", "components.internal-access_specific_rules_you":"(Vous)", + "components.internal-manage_root_users":"Règles d'accès à l'espace de travail partagé", "components.select-users_search_users":"Chercher un utilisateur", "common.access-level_full_acess":"Accès Total", "common.access-level_read":"Lecture", diff --git a/tdrive/frontend/public/locales/ru.json b/tdrive/frontend/public/locales/ru.json index 1f538de17..b19f04507 100644 --- a/tdrive/frontend/public/locales/ru.json +++ b/tdrive/frontend/public/locales/ru.json @@ -192,6 +192,7 @@ "components.internal-access_cannal_info_give_back":"You will need to go to Tdrive chat to give back access to this item.", "components.internal-access_specific_rules":"Specific access rules", "components.internal-access_specific_rules_you":"(you)", + "components.internal-manage_root_users":"Shared drive access rules", "components.select-users_search_users":"Search users", "common.access-level_full_acess":"Full access", "common.access-level_read":"Read", diff --git a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts index 9dc86f7a8..f9ef64aae 100644 --- a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts +++ b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts @@ -175,4 +175,11 @@ export class DriveApiClient { return res; } + + static async updateLevel(companyId: string, id: string, update: any) { + return await Api.post( + `/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}/level`, + update, + ); + } } diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx index fb41ce9c6..cd4dbdc85 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx @@ -6,7 +6,7 @@ import { DriveApiClient } from '../api-client/api-client'; import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store'; import { BrowseFilter, DriveItem, DriveItemVersion } from '../types'; import { SharedWithMeFilterState } from '../state/shared-with-me-filter'; -import Languages from "features/global/services/languages-service"; +import Languages from 'features/global/services/languages-service'; /** * Returns the children of a drive item @@ -21,7 +21,7 @@ export const useDriveActions = () => { ({ set, snapshot }) => async (parentId: string) => { if (parentId) { - const filter:BrowseFilter = { + const filter: BrowseFilter = { company_id: companyId, mime_type: sharedFilter.mimeType.value, }; @@ -110,5 +110,22 @@ export const useDriveActions = () => { [refresh], ); - return { create, refresh, download, downloadZip, remove, update }; + const updateLevel = useCallback( + async (id: string, userId: string, level: string) => { + try { + const updateBody = { + company_id: companyId, + user_id: userId, + level: level + } + await DriveApiClient.updateLevel(companyId, id, updateBody); + await refresh(id || ''); + } catch (e) { + ToasterService.error(Languages.t('hooks.use-drive-actions.unable_update_file')); + } + }, + [refresh], + ); + + return { create, refresh, download, downloadZip, remove, update, updateLevel }; }; diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-item.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-item.tsx index 062f919af..2a5bc1b1a 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-item.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-item.tsx @@ -19,7 +19,7 @@ export const useDriveItem = (id: string) => { const item = useRecoilValue(DriveItemAtom(id)); const children = useRecoilValue(DriveItemChildrenAtom(id)); const [loading, setLoading] = useRecoilState(LoadingStateInitTrue('useDriveItem-' + id)); - const { refresh: refreshItem, create, update: _update, remove: _remove } = useDriveActions(); + const { refresh: refreshItem, create, update: _update, updateLevel: _updateLevel, remove: _remove } = useDriveActions(); const { uploadVersion: _uploadVersion } = useDriveUpload(); const refresh = useCallback( @@ -57,6 +57,19 @@ export const useDriveItem = (id: string) => { [id, setLoading, refresh, item?.item?.parent_id], ); + const updateLevel = useCallback( + async (userId: string, level: string) => { + setLoading(true); + try { + await _updateLevel(id, userId, level); + } catch (e) { + ToasterService.error('Unable to update user access.'); + } + setLoading(false); + }, + [id, setLoading, refresh, item?.item?.parent_id], + ); + const uploadVersion = useCallback( async (file: File) => { setLoading(true); @@ -87,6 +100,7 @@ export const useDriveItem = (id: string) => { uploadVersion, create, update, + updateLevel, remove, refresh, }; diff --git a/tdrive/frontend/src/app/features/users/api/user-api-client.ts b/tdrive/frontend/src/app/features/users/api/user-api-client.ts index 54184efb1..40235a70d 100644 --- a/tdrive/frontend/src/app/features/users/api/user-api-client.ts +++ b/tdrive/frontend/src/app/features/users/api/user-api-client.ts @@ -97,6 +97,28 @@ class UserAPIClientService { return res; } + async all( + companyId: string, + options?: { bufferize?: boolean; callback?: (res: UserType[]) => void }, + ): Promise { + + const res = await new Promise(resolve => + Api.post( + `/internal/services/users/v1/users/${companyId}/all`, + { + include_companies: true, + }, + (res: { resources: UserType[] }): void => { + resolve(res.resources && res.resources.length ? res.resources : []); + }, + ), + ); + + if (options?.callback) options.callback(res); + + return res; + } + async getCurrentUserCompanies(): Promise { return this.listCompanies(CurrentUser.get()?.id || ''); } diff --git a/tdrive/frontend/src/app/features/users/hooks/use-user-company-list.ts b/tdrive/frontend/src/app/features/users/hooks/use-user-company-list.ts new file mode 100644 index 000000000..3e4fb662e --- /dev/null +++ b/tdrive/frontend/src/app/features/users/hooks/use-user-company-list.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; +import useRouterCompany from '@features/router/hooks/use-router-company'; +import { UserType } from '@features/users/types/user'; +import UserAPIClient from '@features/users/api/user-api-client'; + +export const useUserCompanyList = (): UserType[] => { + const companyId = useRouterCompany(); + const [users, setUsers] = useState([]); + const refresh = async () => { + const updatedData = await UserAPIClient.all(companyId); + setUsers(updatedData); + }; + useEffect(() => { + refresh(); + }, []); + return users; +}; diff --git a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx index 67b56b6b2..ceaddd03a 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -30,6 +30,7 @@ import { CreateModalAtom } from './modals/create'; import { PropertiesModal } from './modals/properties'; import { AccessModal } from './modals/update-access'; import { VersionsModal } from './modals/versions'; +import { UsersModal } from './modals/manage-users'; import { SharedFilesTable } from './shared-files-table'; import useRouteState from 'app/features/router/hooks/use-route-state'; import { SharedWithMeFilterState } from '@features/drive/state/shared-with-me-filter'; @@ -59,6 +60,7 @@ export default memo( }) => { const { user } = useCurrentUser(); const companyId = useRouterCompany(); + const role = user ? (user?.companies || []).find(company => company?.company.id === companyId)?.role : "member"; setTdriveTabToken(tdriveTabContextToken || null); const [filter, setFilter] = useRecoilState(SharedWithMeFilterState); @@ -190,6 +192,7 @@ export default memo( > + {role == "admin" && } diff --git a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx index 814a6f9b8..441222bab 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx @@ -8,6 +8,7 @@ import { PropertiesModalAtom } from './modals/properties'; import { SelectorModalAtom } from './modals/selector'; import { AccessModalAtom } from './modals/update-access'; import { VersionsModalAtom } from './modals/versions'; +import { UsersModalAtom } from './modals/manage-users'; import { DriveApiClient, getPublicLinkToken } from '@features/drive/api-client/api-client'; import { useDriveActions } from '@features/drive/hooks/use-drive-actions'; import { getPublicLink } from '@features/drive/hooks/use-drive-item'; @@ -40,6 +41,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s const setVersionModal = useSetRecoilState(VersionsModalAtom); const setAccessModalState = useSetRecoilState(AccessModalAtom); const setPropertiesModalState = useSetRecoilState(PropertiesModalAtom); + const setUsersModalState = useSetRecoilState(UsersModalAtom); const { open: preview } = useDrivePreview(); return useCallback( @@ -266,6 +268,13 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s ); }, }, + { type: 'separator', hide: parent.item!.id != 'root', }, + { + type: 'menu', + text: Languages.t('components.item_context_menu.manage_users'), + hide: parent.item!.id != 'root', + onClick: () => setUsersModalState({ open: true }), + }, { type: 'separator', hide: inTrash || parent.access === 'read' }, { type: 'menu', @@ -301,6 +310,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s setCreationModalState, setVersionModal, setAccessModalState, + setUsersModalState, setPropertiesModalState, ], ); diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/manage-users/common.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/manage-users/common.tsx new file mode 100644 index 000000000..88041b53f --- /dev/null +++ b/tdrive/frontend/src/app/views/client/body/drive/modals/manage-users/common.tsx @@ -0,0 +1,33 @@ +import Select from '@atoms/input/input-select'; +import { DriveFileAccessLevel } from '@features/drive/types'; +import Languages from 'features/global/services/languages-service'; + +export const AccessLevel = ({ + level, + onChange, + canRemove, + hiddenLevels, + className, +}: { + disabled?: boolean; + level: DriveFileAccessLevel | null; + onChange: (level: DriveFileAccessLevel & 'remove') => void; + canRemove?: boolean; + className?: string; + hiddenLevels?: string[]; +}) => { + return ( + + ); +}; diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/manage-users/index.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/manage-users/index.tsx new file mode 100644 index 000000000..09beea607 --- /dev/null +++ b/tdrive/frontend/src/app/views/client/body/drive/modals/manage-users/index.tsx @@ -0,0 +1,86 @@ +import { Modal } from '@atoms/modal'; +import Avatar from '@atoms/avatar'; +import { Base, Info } from '@atoms/text'; +import { atom, useRecoilState } from 'recoil'; +import { useDriveItem } from '@features/drive/hooks/use-drive-item'; +import AlertManager from '@features/global/services/alert-manager-service'; +import { useCurrentUser } from '@features/users/hooks/use-current-user'; +import { useUser } from '@features/users/hooks/use-user'; +import currentUserService from '@features/users/services/current-user-service'; +import { useUserCompanyList } from '@features/users/hooks/use-user-company-list'; +import { AccessLevel } from './common'; +import Languages from 'features/global/services/languages-service'; + +export type UsersModalType = { + open: boolean; +}; + +export const UsersModalAtom = atom({ + key: 'UsersModalAtom', + default: { + open: false, + }, +}); + +export const UsersModal = () => { + const [state, setState] = useRecoilState(UsersModalAtom); + const userList = useUserCompanyList(); + return ( + setState({ open: false })}> + + {Languages.t('components.internal-manage_root_users')} + +
+ {userList?.map(user => ( + + ))} +
+
+ + ); +}; + +const UserAccessLevel = ({ + id, + userId, + username, + role +}: { + id: string; + userId: string; + username: string; + role: string; +}) => { + const user = useUser(userId); + const { user: currentUser } = useCurrentUser(); + const { item, loading, updateLevel } = useDriveItem(id); + const level = role == "admin" ? "manage" : "read"; + + return ( +
+
+ +
+
+ {username} {' '} + {user?.id === currentUser?.id && ( + {Languages.t('components.internal-access_specific_rules_you')} + )} +
+
+ { + updateLevel(user?.id || '', level); + }} + /> +
+
+ ); +}; diff --git a/tdrive/frontend/src/app/views/client/common/disk-usage.tsx b/tdrive/frontend/src/app/views/client/common/disk-usage.tsx index 335018cf2..3e830bc20 100644 --- a/tdrive/frontend/src/app/views/client/common/disk-usage.tsx +++ b/tdrive/frontend/src/app/views/client/common/disk-usage.tsx @@ -22,4 +22,4 @@ export default () => { )} ); -}; +}; \ No newline at end of file diff --git a/tdrive/frontend/src/app/views/client/side-bar/index.tsx b/tdrive/frontend/src/app/views/client/side-bar/index.tsx index 43ce67afc..83741a5c1 100644 --- a/tdrive/frontend/src/app/views/client/side-bar/index.tsx +++ b/tdrive/frontend/src/app/views/client/side-bar/index.tsx @@ -145,4 +145,4 @@ export default () => {
); -}; +}; \ No newline at end of file