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 dd0c64e2..da0e4a55 100644 --- a/tdrive/backend/node/src/services/documents/services/access-check.ts +++ b/tdrive/backend/node/src/services/documents/services/access-check.ts @@ -157,6 +157,7 @@ export const getAccessLevel = async ( : (await isCompanyAdmin(context)) ? "manage" : "write"; + if (id === "shared_with_me") return "read"; //If it is my personal folder, I have full access if (context?.user?.id && id.startsWith("user_")) { diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index dfa6bb05..724901a1 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -16,6 +16,7 @@ import { TYPE as DriveTdriveTabRepoType, } from "../entities/drive-tdrive-tab"; import { + BrowseDetails, CompanyExecutionContext, DocumentsMessageQueueRequest, DriveExecutionContext, @@ -36,6 +37,7 @@ import { getItemName, getPath, getVirtualFoldersNames, + isSharedWithMeFolder, isVirtualFolder, updateItemSize, } from "../utils"; @@ -87,6 +89,27 @@ export class DocumentsService { return this; } + browse = async ( + id: string, + options: SearchDocumentsOptions, + context: DriveExecutionContext & { public_token?: string }, + ): Promise => { + if (isSharedWithMeFolder(id)) { + const children = await this.search(options, context); + return { + access: "read", + children: children.getEntities(), + nextPage: children.nextPage, + path: [] as Array, + }; + } else { + return { + nextPage: null, + ...(await this.get(id, context)), + }; + } + }; + /** * Fetches a drive element * diff --git a/tdrive/backend/node/src/services/documents/types.ts b/tdrive/backend/node/src/services/documents/types.ts index ddc4dd99..9ece0c6c 100644 --- a/tdrive/backend/node/src/services/documents/types.ts +++ b/tdrive/backend/node/src/services/documents/types.ts @@ -1,4 +1,4 @@ -import { ExecutionContext } from "../../core/platform/framework/api/crud-service"; +import { ExecutionContext, Paginable } from "../../core/platform/framework/api/crud-service"; import { DriveFile } from "./entities/drive-file"; import { FileVersion } from "./entities/file-version"; import { SortType } from "../../core/platform/services/search/api"; @@ -25,11 +25,14 @@ export type DriveItemDetails = { access: DriveFileAccessLevel | "none"; }; +export type BrowseDetails = DriveItemDetails & { nextPage: Paginable }; + export type DriveFileAccessLevel = "read" | "write" | "manage"; export type publicAccessLevel = "write" | "read" | "none"; export type RootType = "root"; export type TrashType = "trash"; +export type SharedWithMeType = "shared_with_me"; export type DownloadZipBodyRequest = { items: string[]; diff --git a/tdrive/backend/node/src/services/documents/utils.ts b/tdrive/backend/node/src/services/documents/utils.ts index 487d18a0..2fe6e9e8 100644 --- a/tdrive/backend/node/src/services/documents/utils.ts +++ b/tdrive/backend/node/src/services/documents/utils.ts @@ -17,13 +17,24 @@ 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"; +import { + CompanyExecutionContext, + DriveExecutionContext, + RootType, + SharedWithMeType, + TrashType, +} from "./types"; const ROOT: RootType = "root"; const TRASH: TrashType = "trash"; +const SHARED_WITH_ME: SharedWithMeType = "shared_with_me"; export const isVirtualFolder = (id: string) => { - return id === ROOT || id === TRASH || id.startsWith("user_"); + return id === ROOT || id === TRASH || id.startsWith("user_") || id == SHARED_WITH_ME; +}; + +export const isSharedWithMeFolder = (id: string) => { + return id === SHARED_WITH_ME; }; export const getVirtualFoldersNames = async (id: string, context: DriveExecutionContext) => { 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 a336c606..4570f726 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -141,6 +141,42 @@ export class DocumentsController { }; }; + /** + * Browse file, special endpoint for TDrive application widget. + * Returns the current folder with the filtered content + * + * @param {FastifyRequest} request + * @returns {Promise} + */ + browse = async ( + request: FastifyRequest<{ + Params: ItemRequestParams; + Body: SearchDocumentsBody; + Querystring: PaginationQueryParameters & { public_token?: string }; + }>, + ): Promise => { + const context = getDriveExecutionContext(request); + const { id } = request.params; + + const options: SearchDocumentsOptions = { + ...request.body, + company_id: request.body.company_id || context.company.id, + view: DriveFileDTOBuilder.VIEW_SHARED_WITH_ME, + onlyDirectlyShared: true, + onlyUploadedNotByMe: true, + }; + + return { + ...(await globalResolver.services.documents.documents.browse(id, options, context)), + websockets: request.currentUser?.id + ? globalResolver.platformServices.realtime.sign( + [{ room: `/companies/${context.company.id}/documents/item/${id}` }], + request.currentUser?.id, + ) + : [], + }; + }; + sharedWithMe = async ( request: FastifyRequest<{ Params: RequestParams; diff --git a/tdrive/backend/node/src/services/documents/web/routes.ts b/tdrive/backend/node/src/services/documents/web/routes.ts index 2592e4d2..d07a037d 100644 --- a/tdrive/backend/node/src/services/documents/web/routes.ts +++ b/tdrive/backend/node/src/services/documents/web/routes.ts @@ -94,6 +94,13 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next) handler: documentsController.sharedWithMe.bind(documentsController), }); + fastify.route({ + method: "POST", + url: `${baseUrl}/browse/:id`, + preValidation: [fastify.authenticate], + handler: documentsController.browse.bind(documentsController), + }); + fastify.route({ method: "GET", url: `${baseUrl}/tabs/:tab_id`, 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 3d28167b..1224564a 100644 --- a/tdrive/backend/node/test/e2e/common/common_test_helpers.ts +++ b/tdrive/backend/node/test/e2e/common/common_test_helpers.ts @@ -9,7 +9,7 @@ import {v1 as uuidv1} from "uuid"; import {TestDbService} from "../utils.prepare.db"; import {DriveFile} from "../../../src/services/documents/entities/drive-file"; import {FileVersion} from "../../../src/services/documents/entities/file-version"; -import {SearchResultMockClass} from "./entities/mock_entities"; +import { DriveItemDetailsMockClass, SearchResultMockClass } from "./entities/mock_entities"; import {logger} from "../../../src/core/platform/framework"; export default class TestHelpers { @@ -56,7 +56,7 @@ export default class TestHelpers { return helpers; } - async uploadFiles() { + async uploadFiles(parent_id = "root") { return Promise.all(TestHelpers.ALL_FILES.map(f => this.uploadFile(f))); } @@ -93,24 +93,25 @@ export default class TestHelpers { } async uploadFileAndCreateDocument( - filename: string + filename: string, + parent_id = "root" ) { - return this.uploadFile(filename).then(f => this.createDocumentFromFile(f)); + return this.uploadFile(filename).then(f => this.createDocumentFromFile(f, parent_id)); }; - async uploadRandomFileAndCreateDocument() { - return this.uploadRandomFile().then(f => this.createDocumentFromFile(f)); + async uploadRandomFileAndCreateDocument(parent_id = "root") { + return this.uploadRandomFile().then(f => this.createDocumentFromFile(f, parent_id)); }; - async uploadAllFilesAndCreateDocuments() { - return await Promise.all(TestHelpers.ALL_FILES.map(f => this.uploadFileAndCreateDocument(f))) + async uploadAllFilesAndCreateDocuments(parent_id = "root") { + return await Promise.all(TestHelpers.ALL_FILES.map(f => this.uploadFileAndCreateDocument(f, parent_id))) }; - async uploadAllFilesOneByOne() { + async uploadAllFilesOneByOne(parent_id = "root") { const files: Array = []; for (const idx in TestHelpers.ALL_FILES) { const f = await this.uploadFile(TestHelpers.ALL_FILES[idx]); - const doc = await this.createDocumentFromFile(f); + const doc = await this.createDocumentFromFile(f, parent_id); files.push(doc); } return files; @@ -136,11 +137,12 @@ export default class TestHelpers { }; async createDocumentFromFile( - file: File + file: File, + parent_id = "root", ) { const item = { name: file.metadata.name, - parent_id: "root", + parent_id: parent_id, company_id: file.company_id, }; @@ -158,7 +160,7 @@ export default class TestHelpers { }; async updateDocument( - id: string | "root" | "trash", + id: string | "root" | "trash" | "shared_with_me", item: Partial ) { return await this.platform.app.inject({ @@ -188,6 +190,34 @@ export default class TestHelpers { response.body) }; + async browseDocuments ( + id: string, + payload: Record + ){ + const response = await this.platform.app.inject({ + method: "POST", + url: `${TestHelpers.DOC_URL}/companies/${this.platform.workspace.company_id}/browse/${id}`, + headers: { + authorization: `Bearer ${this.jwt}`, + }, + payload, + }); + + return deserialize( + DriveItemDetailsMockClass, + response.body) + }; + + async getDocument(platform: TestPlatform, id: string | "root" | "trash" | "shared_with_me") { + return await platform.app.inject({ + method: "GET", + url: `${TestHelpers.DOC_URL}/companies/${platform.workspace.company_id}/item/${id}`, + headers: { + authorization: `Bearer ${this.jwt}`, + }, + }); + }; + async sharedWithMeDocuments ( payload: Record ){ diff --git a/tdrive/backend/node/test/e2e/documents/documents-browser.spec.ts b/tdrive/backend/node/test/e2e/documents/documents-browser.spec.ts new file mode 100644 index 00000000..9e15227e --- /dev/null +++ b/tdrive/backend/node/test/e2e/documents/documents-browser.spec.ts @@ -0,0 +1,135 @@ +import { describe, beforeEach, afterEach, it, expect, afterAll } from "@jest/globals"; +import { init, TestPlatform } from "../setup"; +import { TestDbService } from "../utils.prepare.db"; +import TestHelpers from "../common/common_test_helpers"; + +describe("The Documents Browser Window and API", () => { + let platform: TestPlatform; + let currentUser: TestHelpers; + let dbService: TestDbService; + + beforeEach(async () => { + platform = await init({ + services: [ + "webserver", + "database", + "applications", + "search", + "storage", + "message-queue", + "user", + "files", + "auth", + "realtime", + "statistics", + "platform-services", + "documents", + ], + }); + currentUser = await TestHelpers.getInstance(platform); + dbService = await TestDbService.getInstance(platform, true); + }); + + afterAll(async () => { + await platform?.tearDown(); + platform = null; + }); + + describe("My Drive", () => { + + it("Should successfully upload filed to the 'Shared Drive' and browse them", async () => { + const myDriveId = "user_" + currentUser.user.id; + const result = await currentUser.uploadAllFilesOneByOne(myDriveId); + + expect(result).toBeDefined(); + expect(result.entries()).toBeDefined(); + expect(Array.from(result.entries())).toHaveLength(TestHelpers.ALL_FILES.length); + + + const docs = await currentUser.browseDocuments(myDriveId, {}); + expect(docs).toBeDefined(); + expect(docs.children).toBeDefined(); + expect(docs.children.length).toEqual(TestHelpers.ALL_FILES.length) + }); + + it("Should not be visible for other users", async () => { + const myDriveId = "user_" + currentUser.user.id; + const anotherUser = await TestHelpers.getInstance(platform, true); + await currentUser.uploadAllFilesOneByOne(myDriveId); + await new Promise(r => setTimeout(r, 5000)); + + const docs = await currentUser.browseDocuments(myDriveId, {}); + expect(docs).toBeDefined(); + expect(docs.children).toBeDefined(); + expect(docs.children.length).toEqual(TestHelpers.ALL_FILES.length) + + const anotherUserDocs = await anotherUser.searchDocument({}); + expect(anotherUserDocs).toBeDefined(); + expect(anotherUserDocs.entities).toBeDefined(); + expect(anotherUserDocs.entities.length).toEqual(0); + }); + + }); + + describe("Shared Drive", () => { + + it("Should successfully upload filed to the 'Shared Drive' and browse them", async () => { + const result = await currentUser.uploadAllFilesOneByOne("root"); + expect(result).toBeDefined(); + expect(result.entries()).toBeDefined(); + expect(Array.from(result.entries())).toHaveLength(TestHelpers.ALL_FILES.length); + + + const docs = await currentUser.browseDocuments("root", {}); + expect(docs).toBeDefined(); + expect(docs.children).toBeDefined(); + expect(docs.children.length).toEqual(TestHelpers.ALL_FILES.length); + }); + + }); + + describe("Shared With Me", () => { + it("Shouldn't contain files uploaded to the user folder", async () => { + const sharedWIthMeFolder = "shared_with_me"; + await currentUser.uploadAllFilesOneByOne("user_" + currentUser.user.id); + await new Promise(r => setTimeout(r, 5000)); + + let docs = await currentUser.browseDocuments(sharedWIthMeFolder, {}); + expect(docs).toBeDefined(); + expect(docs.children?.length).toEqual(0) + + await currentUser.uploadAllFilesOneByOne("root"); + docs = await currentUser.browseDocuments("shared_with_me", {}); + + expect(docs).toBeDefined(); + expect(docs.children?.length).toEqual(0); + }); + + 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); + let files = await oneUser.uploadAllFilesOneByOne(); + await new Promise(r => setTimeout(r, 5000)); + + //then:: files are not searchable for user without permissions + expect((await anotherUser.browseDocuments("shared_with_me", {})).children).toHaveLength(0); + + //give permissions to the file + files[0].access_info.entities.push({ + type: "user", + id: anotherUser.user.id, + level: "read", + grantor: null, + }); + await oneUser.updateDocument(files[0].id, files[0]); + await new Promise(r => setTimeout(r, 3000)); + + //then file become searchable + expect((await anotherUser.browseDocuments("shared_with_me", {})).children).toHaveLength(1); + }); + }); + + +}); +