make page attachments private

This commit is contained in:
Philipinho
2024-07-04 16:01:35 +01:00
parent a4ec2dac6c
commit 13f26f9c31
6 changed files with 63 additions and 9 deletions

View File

@ -5,6 +5,7 @@ import { getBackendUrl } from "@/lib/config.ts";
const api: AxiosInstance = axios.create({ const api: AxiosInstance = axios.create({
baseURL: getBackendUrl(), baseURL: getBackendUrl(),
withCredentials: true,
}); });
api.interceptors.request.use( api.interceptors.request.use(

View File

@ -31,6 +31,7 @@
"@aws-sdk/client-s3": "^3.600.0", "@aws-sdk/client-s3": "^3.600.0",
"@aws-sdk/s3-request-presigner": "^3.600.0", "@aws-sdk/s3-request-presigner": "^3.600.0",
"@casl/ability": "^6.7.1", "@casl/ability": "^6.7.1",
"@fastify/cookie": "^9.3.1",
"@fastify/multipart": "^8.3.0", "@fastify/multipart": "^8.3.0",
"@fastify/static": "^7.0.4", "@fastify/static": "^7.0.4",
"@nestjs/bullmq": "^10.1.1", "@nestjs/bullmq": "^10.1.1",

View File

@ -45,7 +45,6 @@ import {
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory'; import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo'; import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
import { Public } from '../../common/decorators/public.decorator';
import { validate as isValidUUID } from 'uuid'; import { validate as isValidUUID } from 'uuid';
@Controller() @Controller()
@ -129,12 +128,11 @@ export class AttachmentController {
} }
} }
@Public()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get('/files/:fileId/:fileName') @Get('/files/:fileId/:fileName')
async getFile( async getFile(
@Res() res: FastifyReply, @Res() res: FastifyReply,
//@AuthUser() user: User, @AuthUser() user: User,
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
@Param('fileId') fileId: string, @Param('fileId') fileId: string,
@Param('fileName') fileName?: string, @Param('fileName') fileName?: string,
@ -144,18 +142,29 @@ export class AttachmentController {
} }
const attachment = await this.attachmentRepo.findById(fileId); 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(); throw new NotFoundException();
} }
if (!attachment || !attachment.pageId) { const spaceAbility = await this.spaceAbility.createForUser(
throw new NotFoundException('File record not found'); user,
attachment.spaceId,
);
if (spaceAbility.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
} }
try { try {
const fileStream = await this.storageService.read(attachment.filePath); const fileStream = await this.storageService.read(attachment.filePath);
res.headers({ res.headers({
'Content-Type': getMimeType(attachment.filePath), 'Content-Type': getMimeType(attachment.filePath),
'Cache-Control': 'public, max-age=3600',
}); });
return res.send(fileStream); return res.send(fileStream);
} catch (err) { } catch (err) {
@ -268,6 +277,7 @@ export class AttachmentController {
const fileStream = await this.storageService.read(filePath); const fileStream = await this.storageService.read(filePath);
res.headers({ res.headers({
'Content-Type': getMimeType(filePath), 'Content-Type': getMimeType(filePath),
'Cache-Control': 'public, max-age=86400',
}); });
return res.send(fileStream); return res.send(fileStream);
} catch (err) { } catch (err) {

View File

@ -4,11 +4,12 @@ import {
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt'; import { Strategy } from 'passport-jwt';
import { EnvironmentService } from '../../../integrations/environment/environment.service'; import { EnvironmentService } from '../../../integrations/environment/environment.service';
import { JwtPayload, JwtType } from '../dto/jwt-payload'; import { JwtPayload, JwtType } from '../dto/jwt-payload';
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo'; import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
import { UserRepo } from '@docmost/db/repos/user/user.repo'; import { UserRepo } from '@docmost/db/repos/user/user.repo';
import { FastifyRequest } from 'fastify';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
@ -18,7 +19,15 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
) { ) {
super({ 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, ignoreExpiration: false,
secretOrKey: environmentService.getAppSecret(), secretOrKey: environmentService.getAppSecret(),
passReqToCallback: true, passReqToCallback: true,
@ -50,4 +59,9 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
return { user, workspace }; return { user, workspace };
} }
private extractTokenFromHeader(request: FastifyRequest): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
} }

View File

@ -9,6 +9,7 @@ import { TransformHttpResponseInterceptor } from './common/interceptors/http-res
import fastifyMultipart from '@fastify/multipart'; import fastifyMultipart from '@fastify/multipart';
import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter'; import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter';
import { InternalLogFilter } from './common/logger/internal-log-filter'; import { InternalLogFilter } from './common/logger/internal-log-filter';
import fastifyCookie from '@fastify/cookie';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>( const app = await NestFactory.create<NestFastifyApplication>(
@ -31,6 +32,7 @@ async function bootstrap() {
app.useWebSocketAdapter(redisIoAdapter); app.useWebSocketAdapter(redisIoAdapter);
await app.register(fastifyMultipart as any); await app.register(fastifyMultipart as any);
await app.register(fastifyCookie as any);
app app
.getHttpAdapter() .getHttpAdapter()
@ -56,7 +58,16 @@ async function bootstrap() {
transform: true, 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.useGlobalInterceptors(new TransformHttpResponseInterceptor());
app.enableShutdownHooks(); app.enableShutdownHooks();

17
pnpm-lock.yaml generated
View File

@ -331,6 +331,9 @@ importers:
'@casl/ability': '@casl/ability':
specifier: ^6.7.1 specifier: ^6.7.1
version: 6.7.1 version: 6.7.1
'@fastify/cookie':
specifier: ^9.3.1
version: 9.3.1
'@fastify/multipart': '@fastify/multipart':
specifier: ^8.3.0 specifier: ^8.3.0
version: 8.3.0 version: 8.3.0
@ -1875,6 +1878,9 @@ packages:
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@fastify/cookie@9.3.1':
resolution: {integrity: sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==}
'@fastify/cors@9.0.1': '@fastify/cors@9.0.1':
resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==}
@ -4594,6 +4600,10 @@ packages:
convert-source-map@2.0.0: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 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: cookie@0.4.2:
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -9789,6 +9799,11 @@ snapshots:
'@fastify/busboy@2.1.1': {} '@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': '@fastify/cors@9.0.1':
dependencies: dependencies:
fastify-plugin: 4.5.1 fastify-plugin: 4.5.1
@ -12947,6 +12962,8 @@ snapshots:
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
cookie-signature@1.2.1: {}
cookie@0.4.2: {} cookie@0.4.2: {}
cookie@0.6.0: {} cookie@0.6.0: {}