From 723b77cb55e4878ff5d919ad6fd1089aa4db7c57 Mon Sep 17 00:00:00 2001 From: Romaric Mourgues Date: Fri, 9 Jun 2023 16:03:01 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90=20Initial=20change=20on=20the=20pu?= =?UTF-8?q?blic=20links=20using=20anonymous=20users=20(#69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial change on the public links using anonymous users Co-authored-by: Anton Shepilov --- .../core/platform/services/auth/provider.ts | 1 + .../core/platform/services/auth/service.ts | 9 +- .../core/platform/services/auth/web/jwt.ts | 1 + .../platform/services/email-pusher/index.ts | 2 +- .../search/adapters/elasticsearch/search.ts | 2 +- .../src/core/platform/services/types/index.ts | 1 + .../src/services/console/web/controller.ts | 1 + .../documents/services/access-check.ts | 11 +- .../services/engine/extract-keywords.ts | 2 +- .../src/services/documents/services/index.ts | 22 ++-- .../node/src/services/documents/types.ts | 6 +- .../node/src/services/documents/utils.ts | 2 +- .../documents/web/controllers/documents.ts | 73 +++++++++++-- .../node/src/services/documents/web/routes.ts | 6 ++ .../node/src/services/tags/services/tags.ts | 4 +- .../src/services/tags/web/controllers/tags.ts | 2 +- .../node/src/services/tags/web/routes.ts | 2 +- tdrive/backend/node/src/utils/types.ts | 2 + .../test/e2e/documents/public-links.spec.ts | 101 +++++++++++++++--- .../search-popup/parts/drive-item-result.tsx | 2 +- .../features/drive/api-client/api-client.ts | 42 +++++--- .../app/views/client/body/drive/browser.tsx | 13 ++- .../views/client/body/drive/context-menu.tsx | 4 +- .../app/views/client/body/drive/shared.tsx | 47 ++++++-- .../src/app/views/client/side-bar/actions.tsx | 12 ++- .../src/app/views/client/side-bar/index.tsx | 4 +- 26 files changed, 294 insertions(+), 80 deletions(-) diff --git a/tdrive/backend/node/src/core/platform/services/auth/provider.ts b/tdrive/backend/node/src/core/platform/services/auth/provider.ts index 6dc4b902..fda645d0 100644 --- a/tdrive/backend/node/src/core/platform/services/auth/provider.ts +++ b/tdrive/backend/node/src/core/platform/services/auth/provider.ts @@ -28,6 +28,7 @@ export default interface AuthServiceAPI extends TdriveServiceProvider { track: boolean; provider_id: string; application_id?: string; + public_token_document_id?: string; } & any, ): AccessToken; } diff --git a/tdrive/backend/node/src/core/platform/services/auth/service.ts b/tdrive/backend/node/src/core/platform/services/auth/service.ts index 1fb716fa..1ed15849 100644 --- a/tdrive/backend/node/src/core/platform/services/auth/service.ts +++ b/tdrive/backend/node/src/core/platform/services/auth/service.ts @@ -31,7 +31,12 @@ export class AuthService implements AuthServiceAPI { generateJWT( userId: uuid, email: string, - options: { track: boolean; provider_id: string; application_id?: string }, + options: { + track: boolean; + provider_id: string; + application_id?: string; + public_token_document_id?: string; + }, ): AccessToken { const now = Math.round(new Date().getTime() / 1000); // Current time in UTC assert(this.configuration.expiration, "jwt.expiration is missing"); @@ -53,6 +58,7 @@ export class AuthService implements AuthServiceAPI { track: !!options.track, provider_id: options.provider_id || "", application_id: options.application_id, + public_token_document_id: options.public_token_document_id, } as JwtType), refresh: this.sign({ exp: jwtRefreshExpiration, @@ -64,6 +70,7 @@ export class AuthService implements AuthServiceAPI { track: !!options.track, provider_id: options.provider_id || "", application_id: options.application_id, + public_token_document_id: options.public_token_document_id, } as JwtType), type: "Bearer", }; diff --git a/tdrive/backend/node/src/core/platform/services/auth/web/jwt.ts b/tdrive/backend/node/src/core/platform/services/auth/web/jwt.ts index 7c5d2b55..8af60b6f 100644 --- a/tdrive/backend/node/src/core/platform/services/auth/web/jwt.ts +++ b/tdrive/backend/node/src/core/platform/services/auth/web/jwt.ts @@ -22,6 +22,7 @@ const jwtPlugin: FastifyPluginCallback = (fastify, _opts, next) => { ...{ application_id: jwt.application_id || null }, ...{ server_request: jwt.server_request || false }, ...{ allow_tracking: jwt.track || false }, + ...{ public_token_document_id: jwt.public_token_document_id || null }, }; request.log.debug(`Authenticated as user ${request.currentUser.id}`); }; diff --git a/tdrive/backend/node/src/core/platform/services/email-pusher/index.ts b/tdrive/backend/node/src/core/platform/services/email-pusher/index.ts index a6b61127..968f20b4 100644 --- a/tdrive/backend/node/src/core/platform/services/email-pusher/index.ts +++ b/tdrive/backend/node/src/core/platform/services/email-pusher/index.ts @@ -125,7 +125,7 @@ export default class EmailPusherClass this.logger.info("email sent"); } } catch (error) { - this.logger.error("Failed to send email", error); + this.logger.error({ error: `${error}` }, "Failed to send email"); } } } diff --git a/tdrive/backend/node/src/core/platform/services/search/adapters/elasticsearch/search.ts b/tdrive/backend/node/src/core/platform/services/search/adapters/elasticsearch/search.ts index bfd2c7c7..bfaccad9 100644 --- a/tdrive/backend/node/src/core/platform/services/search/adapters/elasticsearch/search.ts +++ b/tdrive/backend/node/src/core/platform/services/search/adapters/elasticsearch/search.ts @@ -3,7 +3,7 @@ import { TransportRequestOptions } from "@elastic/elasticsearch/lib/Transport"; import { logger } from "../../../../../../core/platform/framework/logger"; import { EntityTarget, FindFilter, FindOptions, getEntityDefinition } from "../../api"; import { asciiFold } from "../utils"; -import { comparisonType } from "src/core/platform/services/database/services/orm/repository/repository"; +import { comparisonType } from "../../../../../../core/platform/services/database/services/orm/repository/repository"; export function buildSearchQuery( entityType: EntityTarget, diff --git a/tdrive/backend/node/src/core/platform/services/types/index.ts b/tdrive/backend/node/src/core/platform/services/types/index.ts index c73670b9..30452e75 100644 --- a/tdrive/backend/node/src/core/platform/services/types/index.ts +++ b/tdrive/backend/node/src/core/platform/services/types/index.ts @@ -9,4 +9,5 @@ export type JwtType = { refresh_nbf?: number; iat: number; track: boolean; + public_token_document_id?: string; }; diff --git a/tdrive/backend/node/src/services/console/web/controller.ts b/tdrive/backend/node/src/services/console/web/controller.ts index 3ba492e6..6ff1fa63 100644 --- a/tdrive/backend/node/src/services/console/web/controller.ts +++ b/tdrive/backend/node/src/services/console/web/controller.ts @@ -126,6 +126,7 @@ export class ConsoleController { { track: request.currentUser?.allow_tracking || false, provider_id: request.currentUser.identity_provider_id, + public_token_document_id: request.currentUser.public_token_document_id, }, ), }; 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 232b462e..27a6603c 100644 --- a/tdrive/backend/node/src/services/documents/services/access-check.ts +++ b/tdrive/backend/node/src/services/documents/services/access-check.ts @@ -43,6 +43,7 @@ export const hasAccessLevel = ( /** * checks the current user is a guest + * Company guests can be users accessing with a public link * * @param {CompanyExecutionContext} context * @returns {Promise} @@ -149,7 +150,7 @@ export const getAccessLevel = async ( context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string }, ): Promise => { if (!id || id === "root") - return !context?.user?.id ? "none" : (await isCompanyGuest(context)) ? "read" : "manage"; + return !context?.user?.id || (await isCompanyGuest(context)) ? "none" : "manage"; if (id === "trash") return (await isCompanyGuest(context)) || !context?.user?.id ? "none" @@ -164,6 +165,7 @@ export const getAccessLevel = async ( } let publicToken = context.public_token; + const prevalidatedPublicTokenDocumentId = context?.user?.public_token_document_id; try { item = @@ -187,10 +189,13 @@ export const getAccessLevel = async ( */ //Public access - if (publicToken) { + if (publicToken || prevalidatedPublicTokenDocumentId === item.id) { 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 (prevalidatedPublicTokenDocumentId) { + return itemLevel; + } if (password) { const data = publicToken.split("+"); if (data.length !== 2) return "none"; @@ -227,7 +232,7 @@ export const getAccessLevel = async ( const matchingCompany = accessEntities.find( a => a.type === "company" && a.id === context.company.id, ); - if (matchingCompany) otherLevels.push(matchingCompany.level); + if (matchingCompany && !isCompanyGuest(context)) otherLevels.push(matchingCompany.level); } //Parent folder diff --git a/tdrive/backend/node/src/services/documents/services/engine/extract-keywords.ts b/tdrive/backend/node/src/services/documents/services/engine/extract-keywords.ts index 37be4a8d..3ca18c51 100644 --- a/tdrive/backend/node/src/services/documents/services/engine/extract-keywords.ts +++ b/tdrive/backend/node/src/services/documents/services/engine/extract-keywords.ts @@ -70,7 +70,7 @@ export class DocumentsProcessor content_keywords = extractKeywords(content_strings); } catch (error) { console.debug(error); - logger.error("Failed to generate content keywords", error); + logger.error({ error: `${error}` }, "Failed to generate content keywords"); } return { content_keywords, item: message.item }; diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 5c5af1b5..4e5462d7 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -81,7 +81,7 @@ export class DocumentsService { DriveTdriveTabEntity, ); } catch (error) { - logger.error("Error while initializing Documents Service", error); + logger.error({ error: `${error}` }, "Error while initializing Documents Service"); } return this; @@ -94,7 +94,10 @@ export class DocumentsService { * @param {DriveExecutionContext} context * @returns {Promise} */ - get = async (id: string, context: DriveExecutionContext): Promise => { + get = async ( + id: string, + context: DriveExecutionContext & { public_token?: string }, + ): Promise => { if (!context) { this.logger.error("invalid context"); return null; @@ -123,11 +126,12 @@ export class DocumentsService { try { const hasAccess = await checkAccess(id, entity, "read", this.repository, context); if (!hasAccess) { - this.logger.error("user does not have access drive item ", id); + this.logger.error("user does not have access drive item " + id); throw Error("user does not have access to this item"); } } catch (error) { - this.logger.error("Failed to grant access to the drive item", error); + console.log(error); + this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item"); throw new CrudException("User does not have access to this item or its children", 401); } @@ -309,7 +313,7 @@ export class DocumentsService { return driveItem; } catch (error) { - this.logger.error("Failed to create drive item", error); + this.logger.error({ error: `${error}` }, "Failed to create drive item"); CrudException.throwMe(error, new CrudException("Failed to create item", 500)); } }; @@ -423,7 +427,7 @@ export class DocumentsService { return item; } catch (error) { console.error(error); - this.logger.error("Failed to update drive item", error); + this.logger.error({ error: `${error}` }, "Failed to update drive item"); throw new CrudException("Failed to update item", 500); } }; @@ -468,7 +472,7 @@ export class DocumentsService { }), ); } catch (error) { - this.logger.error("Failed to empty trash", error); + this.logger.error({ error: `${error}` }, "Failed to empty trash"); throw new CrudException("Failed to empty trash", 500); } @@ -492,7 +496,7 @@ export class DocumentsService { throw Error("user does not have access to this item"); } } catch (error) { - this.logger.error("Failed to grant access to the drive item", error); + this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item"); throw new CrudException("User does not have access to this item or its children", 401); } @@ -626,7 +630,7 @@ export class DocumentsService { return driveItemVersion; } catch (error) { - this.logger.error("Failed to create Drive item version", error); + this.logger.error({ error: `${error}` }, "Failed to create Drive item version"); throw new CrudException("Failed to create Drive item version", 500); } }; diff --git a/tdrive/backend/node/src/services/documents/types.ts b/tdrive/backend/node/src/services/documents/types.ts index 40463fe6..cf486d6d 100644 --- a/tdrive/backend/node/src/services/documents/types.ts +++ b/tdrive/backend/node/src/services/documents/types.ts @@ -1,15 +1,13 @@ import { ExecutionContext } from "../../core/platform/framework/api/crud-service"; import { DriveFile } from "./entities/drive-file"; import { FileVersion } from "./entities/file-version"; -import { SortType } from "src/core/platform/services/search/api"; +import { SortType } from "../../core/platform/services/search/api"; export interface CompanyExecutionContext extends ExecutionContext { company: { id: string }; } -export type DriveExecutionContext = CompanyExecutionContext & { - public_token?: string; -}; +export type DriveExecutionContext = CompanyExecutionContext; export type RequestParams = { company_id: string; diff --git a/tdrive/backend/node/src/services/documents/utils.ts b/tdrive/backend/node/src/services/documents/utils.ts index 09d9f4ab..6bc681dd 100644 --- a/tdrive/backend/node/src/services/documents/utils.ts +++ b/tdrive/backend/node/src/services/documents/utils.ts @@ -228,7 +228,7 @@ export const getPath = async ( ): Promise => { id = id || "root"; if (isVirtualFolder(id)) - return !context.public_token || ignoreAccess + return !context?.user?.public_token_document_id || ignoreAccess ? [ { id, 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 1eea4de3..bfd3c3ad 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -1,4 +1,5 @@ import { FastifyReply, FastifyRequest } from "fastify"; +import { getInstance } from "../../../../services/user/entities/user"; import { logger } from "../../../../core/platform/framework"; import { CrudException, ListResult } from "../../../../core/platform/framework/api/crud-service"; import { File } from "../../../../services/files/entities/file"; @@ -68,7 +69,7 @@ export class DocumentsController { context, ); } catch (error) { - logger.error("Failed to create Drive item", error); + logger.error({ error: `${error}` }, "Failed to create Drive item"); CrudException.throwMe(error, new CrudException("Failed to create Drive item", 500)); } }; @@ -91,7 +92,7 @@ export class DocumentsController { reply.status(200).send(); } catch (error) { - logger.error("Failed to delete drive item", error); + logger.error({ error: `${error}` }, "Failed to delete drive item"); throw new CrudException("Failed to delete drive item", 500); } }; @@ -281,7 +282,7 @@ export class DocumentsController { response.send(data.file); } } catch (error) { - logger.error("failed to download file", error); + logger.error({ error: `${error}` }, "failed to download file"); throw new CrudException("Failed to download file", 500); } }; @@ -328,7 +329,7 @@ export class DocumentsController { archive.pipe(reply.raw); } catch (error) { - logger.error("failed to send zip file", error); + logger.error({ error: `${error}` }, "failed to send zip file"); throw new CrudException("Failed to create zip file", 500); } }; @@ -361,7 +362,7 @@ export class DocumentsController { return this.driveFileDTOBuilder.build(fileList, context, options.fields, options.view); } catch (error) { - logger.error("error while searching for document", error); + logger.error({ error: `${error}` }, "error while searching for document"); this.throw500Search(); } }; @@ -401,6 +402,65 @@ export class DocumentsController { context, ); }; + + async getAnonymousToken( + req: FastifyRequest<{ + Body: { + company_id: string; + document_id: string; + token: string; + token_password?: string; + }; + }>, + ): Promise<{ + access_token: { + time: number; + expiration: number; + refresh_expiration: number; + value: string; + refresh: string; + type: string; + }; + }> { + const document = await globalResolver.services.documents.documents.get(req.body.document_id, { + public_token: req.body.token + (req.body.token_password ? "+" + req.body.token_password : ""), + user: null, + company: { id: req.body.company_id }, + }); + + if (!document || !document.access || document.access === "none") + throw new CrudException("You don't have access to this document", 401); + + const email = document.item.company_id + "-anonymous@tdrive.com"; + let user = await globalResolver.services.users.getByEmail(email); + if (!user) { + user = ( + await globalResolver.services.users.create( + getInstance({ + first_name: "Anonymous", + last_name: "", + email_canonical: email, + username_canonical: (email.replace("@", ".") || "").toLocaleLowerCase(), + phone: "", + identity_provider: "anonymous", + identity_provider_id: email, + mail_verified: true, + }), + ) + ).entity; + } + await globalResolver.services.companies.setUserRole(document.item.company_id, user.id, "guest"); + + const token = globalResolver.platformServices.auth.generateJWT(user.id, user.email_canonical, { + track: false, + provider_id: "tdrive", + public_token_document_id: req.body.document_id, + }); + + return { + access_token: token, + }; + } } /** @@ -410,9 +470,8 @@ export class DocumentsController { * @returns {CompanyExecutionContext} */ const getDriveExecutionContext = ( - req: FastifyRequest<{ Params: { company_id: string }; Querystring?: { public_token?: string } }>, + req: FastifyRequest<{ Params: { company_id: string } }>, ): DriveExecutionContext => ({ - public_token: req.query?.public_token, user: req.currentUser, company: { id: req.params.company_id }, url: req.url, diff --git a/tdrive/backend/node/src/services/documents/web/routes.ts b/tdrive/backend/node/src/services/documents/web/routes.ts index 29427e39..1b5d79ed 100644 --- a/tdrive/backend/node/src/services/documents/web/routes.ts +++ b/tdrive/backend/node/src/services/documents/web/routes.ts @@ -101,6 +101,12 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next) handler: documentsController.setTab.bind(documentsController), }); + fastify.route({ + method: "POST", + url: `${baseUrl}/anonymous/token`, + handler: documentsController.getAnonymousToken.bind(documentsController), + }); + return next(); }; diff --git a/tdrive/backend/node/src/services/tags/services/tags.ts b/tdrive/backend/node/src/services/tags/services/tags.ts index debb1a7f..ba4d34d1 100644 --- a/tdrive/backend/node/src/services/tags/services/tags.ts +++ b/tdrive/backend/node/src/services/tags/services/tags.ts @@ -1,5 +1,5 @@ -import { TdriveServiceProvider, Initializable } from "src/core/platform/framework/api"; -import Repository from "src/core/platform/services/database/services/orm/repository/repository"; +import { TdriveServiceProvider, Initializable } from "../../../core/platform/framework/api"; +import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; import { Tag, TagsType, TagsPrimaryKey } from "../entities"; import gr from "../../global-resolver"; import { diff --git a/tdrive/backend/node/src/services/tags/web/controllers/tags.ts b/tdrive/backend/node/src/services/tags/web/controllers/tags.ts index 7fba2fb6..90ebf81c 100644 --- a/tdrive/backend/node/src/services/tags/web/controllers/tags.ts +++ b/tdrive/backend/node/src/services/tags/web/controllers/tags.ts @@ -8,7 +8,7 @@ import { } from "../../../../utils/types"; import { Tag } from "../../entities"; import gr from "../../../global-resolver"; -import { ExecutionContext } from "src/core/platform/framework/api/crud-service"; +import { ExecutionContext } from "../../../../core/platform/framework/api/crud-service"; import { handleError } from "../../../../utils/handleError"; export class TagsController diff --git a/tdrive/backend/node/src/services/tags/web/routes.ts b/tdrive/backend/node/src/services/tags/web/routes.ts index 35ab58f4..7fa76102 100644 --- a/tdrive/backend/node/src/services/tags/web/routes.ts +++ b/tdrive/backend/node/src/services/tags/web/routes.ts @@ -3,7 +3,7 @@ import { checkUserBelongsToCompany } from "../../../utils/company"; import { Tag } from "../types"; import { TagsController } from "./controllers"; import gr from "../../global-resolver"; -import CompanyUser from "src/services/user/entities/company_user"; +import CompanyUser from "../../../services/user/entities/company_user"; const tagsUrl = "/companies/:company_id/tags"; diff --git a/tdrive/backend/node/src/utils/types.ts b/tdrive/backend/node/src/utils/types.ts index 015fe5b1..d4b2f1e7 100644 --- a/tdrive/backend/node/src/utils/types.ts +++ b/tdrive/backend/node/src/utils/types.ts @@ -28,6 +28,8 @@ export interface User { application_id?: string; // allow_tracking allow_tracking?: boolean; + // Prevalidated public token access + public_token_document_id?: string; } export const webSocketSchema = { 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 feec7b0a..985c381e 100644 --- a/tdrive/backend/node/test/e2e/documents/public-links.spec.ts +++ b/tdrive/backend/node/test/e2e/documents/public-links.spec.ts @@ -22,6 +22,17 @@ describe("the public links feature", () => { access_info: AccessInformation; } + class AccessTokenMockClass { + access_token: { + time: 0; + expiration: number; + refresh_expiration: number; + value: string; + refresh: string; + type: "Bearer"; + }; + } + class FullDriveInfoMockClass { path: DriveFile[]; item?: DriveFile; @@ -112,10 +123,28 @@ describe("the public links feature", () => { const file = deserialize(DriveFileMockClass, res.body); expect(file.access_info.public?.level).toBe("read"); + const accessRes = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${publicFile.company_id}/anonymous/token`, + headers: {}, + payload: { + company_id: publicFile.company_id, + document_id: publicFile.id, + token: publicFile.access_info.public?.token, + }, + }); + const { access_token } = deserialize( + AccessTokenMockClass, + accessRes.body, + ); + expect(access_token).toBeDefined(); + const resPublicRaw = await platform.app.inject({ method: "GET", - url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`, - headers: {}, + url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}`, + headers: { + Authorization: `Bearer ${access_token.value}`, + }, }); const resPublic = deserialize(FullDriveInfoMockClass, resPublicRaw.body); expect(resPublicRaw.statusCode).toBe(200); @@ -135,10 +164,27 @@ describe("the public links feature", () => { }, }); + const accessRes = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${publicFile.company_id}/anonymous/token`, + headers: {}, + payload: { + company_id: publicFile.company_id, + document_id: publicFile.id, + token: publicFile.access_info.public?.token, + }, + }); + const { access_token } = deserialize( + AccessTokenMockClass, + accessRes.body, + ); + let resPublicRaw = await platform.app.inject({ method: "GET", - url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`, - headers: {}, + url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}`, + headers: { + Authorization: `Bearer ${access_token.value}`, + }, }); const resPublic = deserialize(FullDriveInfoMockClass, resPublicRaw.body); expect(resPublicRaw.statusCode).toBe(200); @@ -158,8 +204,10 @@ describe("the public links feature", () => { resPublicRaw = await platform.app.inject({ method: "GET", - url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`, - headers: {}, + url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}`, + headers: { + authorization: `Bearer ${access_token.value}`, + }, }); expect(resPublicRaw.statusCode).toBe(401); @@ -189,22 +237,45 @@ describe("the public links feature", () => { }, }); + const badAccessRes = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${publicFile.company_id}/anonymous/token`, + headers: {}, + payload: { + company_id: publicFile.company_id, + document_id: publicFile.id, + token: publicFile.access_info.public?.token, + }, + }); + expect(badAccessRes.statusCode).toBe(401); + + const accessRes = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${publicFile.company_id}/anonymous/token`, + headers: {}, + payload: { + company_id: publicFile.company_id, + document_id: publicFile.id, + token: publicFile.access_info.public?.token, + token_password: "abcdef", + }, + }); + const { access_token } = deserialize( + AccessTokenMockClass, + accessRes.body, + ); + let resPublicRaw = await platform.app.inject({ method: "GET", - url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}%2Babcdef`, - headers: {}, + url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}`, + headers: { + Authorization: `Bearer ${access_token.value}`, + }, }); let resPublic = deserialize(FullDriveInfoMockClass, resPublicRaw.body); expect(resPublicRaw.statusCode).toBe(200); expect(resPublic.item?.id).toBe(publicFile.id); - resPublicRaw = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`, - headers: {}, - }); - expect(resPublicRaw.statusCode).toBe(401); - await e2e_updateDocument(platform, publicFile.id, { ...publicFile, access_info: { diff --git a/tdrive/frontend/src/app/components/search-popup/parts/drive-item-result.tsx b/tdrive/frontend/src/app/components/search-popup/parts/drive-item-result.tsx index 3893254b..8e0b3ad4 100644 --- a/tdrive/frontend/src/app/components/search-popup/parts/drive-item-result.tsx +++ b/tdrive/frontend/src/app/components/search-popup/parts/drive-item-result.tsx @@ -32,7 +32,7 @@ export default (props: { driveItem: DriveItem & { user?: UserType } }) => { const input = useRecoilValue(SearchInputState); const currentWorkspaceId = useRouterWorkspace(); const companyApplications = useCompanyApplications(); - const [_, setParentId] = useRecoilState(DriveCurrentFolderAtom('root')); + const [_, setParentId] = useRecoilState(DriveCurrentFolderAtom({ initialFolderId: 'root' })); const tdriveDriveApplicationId = companyApplications.applications.find(application => { return application.identity.code === 'tdrive_drive'; 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 eb967d4a..4cda0676 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 @@ -2,6 +2,7 @@ import Api from '../../global/framework/api-service'; import { DriveItem, DriveItemDetails, DriveItemVersion } from '../types'; import Workspace from '@deprecated/workspaces/workspaces'; import Logger from 'features/global/framework/logger-service'; +import { JWTDataType } from 'app/features/auth/jwt-storage-service'; export interface BaseSearchOptions { company_id?: string; workspace_id?: string; @@ -32,10 +33,7 @@ export const setTdriveTabToken = (token: string | null) => { tdriveTabToken = token; }; -const appendPublicAndTdriveToken = (useAnd?: boolean) => { - if (publicLinkToken) { - return `${useAnd ? '&' : '?'}public_token=${publicLinkToken}`; - } +const appendTdriveToken = (useAnd?: boolean) => { if (tdriveTabToken) { return `${useAnd ? '&' : '?'}tdrive_tab_token=${tdriveTabToken}`; } @@ -44,21 +42,39 @@ const appendPublicAndTdriveToken = (useAnd?: boolean) => { export class DriveApiClient { private static logger = Logger.getLogger('MessageAPIClientService'); + + static async getAnonymousToken( + companyId: string, + id: string, + publicToken: string, + password?: string, + ) { + return await Api.post< + { company_id: string; document_id: string; token: string; token_password?: string }, + { access_token: JWTDataType } + >(`/internal/services/documents/v1/companies/${companyId}/anonymous/token`, { + company_id: companyId, + document_id: id, + token: publicToken, + token_password: password, + }); + } + static async get(companyId: string, id: string | 'trash' | '') { return await Api.get( - `/internal/services/documents/v1/companies/${companyId}/item/${id}${appendPublicAndTdriveToken()}`, + `/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}`, ); } static async remove(companyId: string, id: string | 'trash' | '') { return await Api.delete( - `/internal/services/documents/v1/companies/${companyId}/item/${id}${appendPublicAndTdriveToken()}`, + `/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}`, ); } static async update(companyId: string, id: string, update: Partial) { return await Api.post, DriveItem>( - `/internal/services/documents/v1/companies/${companyId}/item/${id}${appendPublicAndTdriveToken()}`, + `/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}`, update, ); } @@ -72,14 +88,14 @@ export class DriveApiClient { { item: Partial; version: Partial }, DriveItem >( - `/internal/services/documents/v1/companies/${companyId}/item${appendPublicAndTdriveToken()}`, + `/internal/services/documents/v1/companies/${companyId}/item${appendTdriveToken()}`, data as { item: Partial; version: Partial }, ); } static async createVersion(companyId: string, id: string, version: Partial) { return await Api.post, DriveItemVersion>( - `/internal/services/documents/v1/companies/${companyId}/item/${id}/version${appendPublicAndTdriveToken()}`, + `/internal/services/documents/v1/companies/${companyId}/item/${id}/version${appendTdriveToken()}`, version, ); } @@ -88,7 +104,7 @@ export class DriveApiClient { return Api.get<{ token: string }>( `/internal/services/documents/v1/companies/${companyId}/item/download/token` + `?items=${ids.join(',')}&version_id=${versionId}` + - appendPublicAndTdriveToken(true), + appendTdriveToken(true), ); } @@ -96,12 +112,12 @@ export class DriveApiClient { const { token } = await DriveApiClient.getDownloadToken(companyId, [id], versionId); if (versionId) return Api.route( - `/internal/services/documents/v1/companies/${companyId}/item/${id}/download?version_id=${versionId}&token=${token}${appendPublicAndTdriveToken( + `/internal/services/documents/v1/companies/${companyId}/item/${id}/download?version_id=${versionId}&token=${token}${appendTdriveToken( true, )}`, ); return Api.route( - `/internal/services/documents/v1/companies/${companyId}/item/${id}/download?token=${token}${appendPublicAndTdriveToken( + `/internal/services/documents/v1/companies/${companyId}/item/${id}/download?token=${token}${appendTdriveToken( true, )}`, ); @@ -112,7 +128,7 @@ export class DriveApiClient { return Api.route( `/internal/services/documents/v1/companies/${companyId}/item/download/zip` + `?items=${ids.join(',')}&token=${token}` + - appendPublicAndTdriveToken(true), + appendTdriveToken(true), ); } 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 6114173b..40c1ea0f 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -26,17 +26,22 @@ import { PropertiesModal } from './modals/properties'; import { AccessModal } from './modals/update-access'; import { VersionsModal } from './modals/versions'; -export const DriveCurrentFolderAtom = atomFamily({ +export const DriveCurrentFolderAtom = atomFamily< + string, + { context?: string; initialFolderId: string } +>({ key: 'DriveCurrentFolderAtom', - default: startingParentId => startingParentId || 'root', + default: options => options.initialFolderId || 'root', }); export default memo( ({ + context, initialParentId, tdriveTabContextToken, inPublicSharing, }: { + context?: string; initialParentId?: string; tdriveTabContextToken?: string; inPublicSharing?: boolean; @@ -45,7 +50,7 @@ export default memo( setTdriveTabToken(tdriveTabContextToken || null); const [parentId, _setParentId] = useRecoilState( - DriveCurrentFolderAtom(initialParentId || 'root'), + DriveCurrentFolderAtom({ context: context, initialFolderId: initialParentId || 'root' }), ); const [loadingParentChange, setLoadingParentChange] = useState(false); @@ -118,7 +123,7 @@ export default memo( return ( checkedIds[c.id]); - const [_, setParentId] = useRecoilState(DriveCurrentFolderAtom(initialParentId || 'root')); + const [_, setParentId] = useRecoilState( + DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'root' }), + ); const { download, downloadZip, update } = useDriveActions(); const setCreationModalState = useSetRecoilState(CreateModalAtom); diff --git a/tdrive/frontend/src/app/views/client/body/drive/shared.tsx b/tdrive/frontend/src/app/views/client/body/drive/shared.tsx index b979be2e..796acfa1 100755 --- a/tdrive/frontend/src/app/views/client/body/drive/shared.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/shared.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import MenusBodyLayer from '@components/menus/menus-body-layer'; +import JWTStorage from '@features/auth/jwt-storage-service'; import Api from '@features/global/framework/api-service'; import Languages from '@features/global/services/languages-service'; import { addApiUrlIfNeeded } from '@features/global/utils/URLUtils'; @@ -11,15 +12,18 @@ import { Input } from 'app/atoms/input/input-text'; import { Base, Subtitle, Title } from 'app/atoms/text'; import UploadsViewer from 'app/components/file-uploads/uploads-viewer'; import { useDriveItem } from 'app/features/drive/hooks/use-drive-item'; -import { useDriveUpload } from 'app/features/drive/hooks/use-drive-upload'; +import { ToasterService } from 'app/features/global/services/toaster-service'; import { useParams } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; import shortUUID from 'short-uuid'; import Avatar from '../../../../atoms/avatar'; -import { setPublicLinkToken } from '../../../../features/drive/api-client/api-client'; +import { + DriveApiClient, + setPublicLinkToken, +} from '../../../../features/drive/api-client/api-client'; import useRouterCompany from '../../../../features/router/hooks/use-router-company'; -import { CreateModalAtom } from './modals/create'; import { CreateModalWithUploadZones } from '../../side-bar/actions'; +import { useRecoilState } from 'recoil'; +import { DriveCurrentFolderAtom } from './browser'; export default () => { const companyId = useRouterCompany(); @@ -44,7 +48,6 @@ export default () => { const { token, documentId: _documentId } = useParams() as { token?: string; documentId?: string }; const documentId = _documentId ? shortUUID().toUUID(_documentId || '') : ''; - setPublicLinkToken(token || null); if (!companyId) { return <>; @@ -77,7 +80,7 @@ export default () => { - + ); }; @@ -92,10 +95,36 @@ const AccessChecker = ({ folderId: string; }) => { const { details, loading, refresh } = useDriveItem(folderId); + const companyId = useRouterCompany(); const [password, setPassword] = useState((token || '').split('+')[1] || ''); + const setPublicToken = async (token: string, password?: string) => { + try { + setPublicLinkToken(token || null); + + const { access_token } = await DriveApiClient.getAnonymousToken( + companyId, + folderId, + token, + password, + ); + + if (!access_token) { + throw new Error('Invalid password or token, or expired link.'); + } + + JWTStorage.updateJWT(access_token); + } catch (e) { + console.error(e); + ToasterService.error('Unable to access documents: ' + e); + } + }; + useEffect(() => { - refresh(folderId); + (async () => { + await setPublicToken(token || ''); + refresh(folderId); + })(); }, []); if (!details?.item?.id && loading) { @@ -125,8 +154,8 @@ const AccessChecker = ({