From 13f26f9c315a625ee5ecd2428fb387ef44d202de Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:01:35 +0100 Subject: [PATCH] make page attachments private --- apps/client/src/lib/api-client.ts | 1 + apps/server/package.json | 1 + .../core/attachment/attachment.controller.ts | 22 ++++++++++++++----- .../src/core/auth/strategies/jwt.strategy.ts | 18 +++++++++++++-- apps/server/src/main.ts | 13 ++++++++++- pnpm-lock.yaml | 17 ++++++++++++++ 6 files changed, 63 insertions(+), 9 deletions(-) diff --git a/apps/client/src/lib/api-client.ts b/apps/client/src/lib/api-client.ts index cdf37ea5..83b2f0cc 100644 --- a/apps/client/src/lib/api-client.ts +++ b/apps/client/src/lib/api-client.ts @@ -5,6 +5,7 @@ import { getBackendUrl } from "@/lib/config.ts"; const api: AxiosInstance = axios.create({ baseURL: getBackendUrl(), + withCredentials: true, }); api.interceptors.request.use( diff --git a/apps/server/package.json b/apps/server/package.json index dea503ad..d025808d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -31,6 +31,7 @@ "@aws-sdk/client-s3": "^3.600.0", "@aws-sdk/s3-request-presigner": "^3.600.0", "@casl/ability": "^6.7.1", + "@fastify/cookie": "^9.3.1", "@fastify/multipart": "^8.3.0", "@fastify/static": "^7.0.4", "@nestjs/bullmq": "^10.1.1", diff --git a/apps/server/src/core/attachment/attachment.controller.ts b/apps/server/src/core/attachment/attachment.controller.ts index b3231597..3f0ab177 100644 --- a/apps/server/src/core/attachment/attachment.controller.ts +++ b/apps/server/src/core/attachment/attachment.controller.ts @@ -45,7 +45,6 @@ import { import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo'; -import { Public } from '../../common/decorators/public.decorator'; import { validate as isValidUUID } from 'uuid'; @Controller() @@ -129,12 +128,11 @@ export class AttachmentController { } } - @Public() @UseGuards(JwtAuthGuard) @Get('/files/:fileId/:fileName') async getFile( @Res() res: FastifyReply, - //@AuthUser() user: User, + @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, @Param('fileId') fileId: string, @Param('fileName') fileName?: string, @@ -144,18 +142,29 @@ export class AttachmentController { } const attachment = await this.attachmentRepo.findById(fileId); - if (attachment.workspaceId !== workspace.id) { + if ( + !attachment || + attachment.workspaceId !== workspace.id || + !attachment.pageId || + !attachment.spaceId + ) { throw new NotFoundException(); } - if (!attachment || !attachment.pageId) { - throw new NotFoundException('File record not found'); + const spaceAbility = await this.spaceAbility.createForUser( + user, + attachment.spaceId, + ); + + if (spaceAbility.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); } try { const fileStream = await this.storageService.read(attachment.filePath); res.headers({ 'Content-Type': getMimeType(attachment.filePath), + 'Cache-Control': 'public, max-age=3600', }); return res.send(fileStream); } catch (err) { @@ -268,6 +277,7 @@ export class AttachmentController { const fileStream = await this.storageService.read(filePath); res.headers({ 'Content-Type': getMimeType(filePath), + 'Cache-Control': 'public, max-age=86400', }); return res.send(fileStream); } catch (err) { diff --git a/apps/server/src/core/auth/strategies/jwt.strategy.ts b/apps/server/src/core/auth/strategies/jwt.strategy.ts index f58be825..7abebd1f 100644 --- a/apps/server/src/core/auth/strategies/jwt.strategy.ts +++ b/apps/server/src/core/auth/strategies/jwt.strategy.ts @@ -4,11 +4,12 @@ import { UnauthorizedException, } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Strategy } from 'passport-jwt'; import { EnvironmentService } from '../../../integrations/environment/environment.service'; import { JwtPayload, JwtType } from '../dto/jwt-payload'; import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo'; import { UserRepo } from '@docmost/db/repos/user/user.repo'; +import { FastifyRequest } from 'fastify'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { @@ -18,7 +19,15 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { private readonly environmentService: EnvironmentService, ) { super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + jwtFromRequest: (req: FastifyRequest) => { + let accessToken = null; + + try { + accessToken = JSON.parse(req.cookies?.authTokens)?.accessToken; + } catch {} + + return accessToken || this.extractTokenFromHeader(req); + }, ignoreExpiration: false, secretOrKey: environmentService.getAppSecret(), passReqToCallback: true, @@ -50,4 +59,9 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { return { user, workspace }; } + + private extractTokenFromHeader(request: FastifyRequest): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } } diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index fd276ac1..c3e180a2 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -9,6 +9,7 @@ import { TransformHttpResponseInterceptor } from './common/interceptors/http-res import fastifyMultipart from '@fastify/multipart'; import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter'; import { InternalLogFilter } from './common/logger/internal-log-filter'; +import fastifyCookie from '@fastify/cookie'; async function bootstrap() { const app = await NestFactory.create( @@ -31,6 +32,7 @@ async function bootstrap() { app.useWebSocketAdapter(redisIoAdapter); await app.register(fastifyMultipart as any); + await app.register(fastifyCookie as any); app .getHttpAdapter() @@ -56,7 +58,16 @@ async function bootstrap() { transform: true, }), ); - app.enableCors(); + + if (process.env.NODE_ENV !== 'production') { + // make development easy + app.enableCors({ + origin: ['http://localhost:5173'], + credentials: true, + }); + } else { + app.enableCors(); + } app.useGlobalInterceptors(new TransformHttpResponseInterceptor()); app.enableShutdownHooks(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3057a1a7..6905a1c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,6 +331,9 @@ importers: '@casl/ability': specifier: ^6.7.1 version: 6.7.1 + '@fastify/cookie': + specifier: ^9.3.1 + version: 9.3.1 '@fastify/multipart': specifier: ^8.3.0 version: 8.3.0 @@ -1875,6 +1878,9 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@fastify/cookie@9.3.1': + resolution: {integrity: sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==} + '@fastify/cors@9.0.1': resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} @@ -4594,6 +4600,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.1: + resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==} + engines: {node: '>=6.6.0'} + cookie@0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} @@ -9789,6 +9799,11 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@fastify/cookie@9.3.1': + dependencies: + cookie-signature: 1.2.1 + fastify-plugin: 4.5.1 + '@fastify/cors@9.0.1': dependencies: fastify-plugin: 4.5.1 @@ -12947,6 +12962,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-signature@1.2.1: {} + cookie@0.4.2: {} cookie@0.6.0: {}