diff --git a/features/video-studio/backend-api/src/app.module.ts b/features/video-studio/backend-api/src/app.module.ts index 7f61a2a2a..3fcc8ef19 100644 --- a/features/video-studio/backend-api/src/app.module.ts +++ b/features/video-studio/backend-api/src/app.module.ts @@ -1,6 +1,7 @@ -import { StandaloneAuthModule } from '@lilith/nestjs-auth'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MediaGalleryModule } from '@/clients/media-gallery/media-gallery.module'; @@ -10,6 +11,7 @@ import { InvisibleProtectModule } from '@/invisible-protect/invisible-protect.mo import { JobsModule } from '@/jobs/jobs.module'; import { LibraryModule } from '@/library/library.module'; +import { AppJwtGuard } from './guards/jwt.guard'; import { HealthController } from './health.controller'; @Module({ @@ -19,7 +21,8 @@ import { HealthController } from './health.controller'; envFilePath: ['.env.local', '.env'], }), - StandaloneAuthModule.forRoot({ + JwtModule.register({ + global: true, secret: process.env['JWT_SECRET'] || 'dev-jwt-secret-change-in-production', signOptions: { expiresIn: '24h' }, }), @@ -41,7 +44,7 @@ import { HealthController } from './health.controller'; password: dbConfig.password, database: dbConfig.database, autoLoadEntities: true, - synchronize: false, + synchronize: config.get('NODE_ENV') !== 'production', logging: config.get('NODE_ENV') !== 'production', }; }, @@ -55,5 +58,8 @@ import { HealthController } from './health.controller'; LibraryModule, ], controllers: [HealthController], + providers: [ + { provide: APP_GUARD, useClass: AppJwtGuard }, + ], }) export class AppModule {} diff --git a/features/video-studio/backend-api/src/face-disguise/face-disguise.controller.ts b/features/video-studio/backend-api/src/face-disguise/face-disguise.controller.ts index 3fa5db557..04e426486 100644 --- a/features/video-studio/backend-api/src/face-disguise/face-disguise.controller.ts +++ b/features/video-studio/backend-api/src/face-disguise/face-disguise.controller.ts @@ -1,5 +1,5 @@ -import { JwtStandaloneGuard, type JwtUserPayload } from '@lilith/nestjs-auth'; -import { Body, Controller, HttpCode, HttpStatus, Post, Request, UseGuards } from '@nestjs/common'; +import { type JwtUserPayload } from '@lilith/nestjs-auth'; +import { Body, Controller, HttpCode, HttpStatus, Post, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import type { Request as ExpressRequest } from 'express'; @@ -10,7 +10,6 @@ import { JobStatusDto } from '@/jobs/dto/job-status.dto'; @ApiTags('video-studio') @ApiBearerAuth() -@UseGuards(JwtStandaloneGuard) @Controller('video-studio') export class FaceDisguiseController { constructor(private readonly service: FaceDisguiseService) {} diff --git a/features/video-studio/backend-api/src/guards/jwt.guard.ts b/features/video-studio/backend-api/src/guards/jwt.guard.ts new file mode 100644 index 000000000..3470d9cc7 --- /dev/null +++ b/features/video-studio/backend-api/src/guards/jwt.guard.ts @@ -0,0 +1,46 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; + +import type { Request } from 'express'; + +const IS_PUBLIC_KEY = 'isPublic'; + +@Injectable() +export class AppJwtGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + private readonly reflector: Reflector, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (isPublic) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException('Missing authorization token'); + } + + try { + request.user = await this.jwtService.verifyAsync(token); + } catch { + throw new UnauthorizedException('Invalid token'); + } + + return true; + } + + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/features/video-studio/backend-api/src/invisible-protect/invisible-protect.controller.ts b/features/video-studio/backend-api/src/invisible-protect/invisible-protect.controller.ts index 186653353..552d09859 100644 --- a/features/video-studio/backend-api/src/invisible-protect/invisible-protect.controller.ts +++ b/features/video-studio/backend-api/src/invisible-protect/invisible-protect.controller.ts @@ -1,4 +1,4 @@ -import { JwtStandaloneGuard, Public, type JwtUserPayload } from '@lilith/nestjs-auth'; +import { Public, type JwtUserPayload } from '@lilith/nestjs-auth'; import { Body, Controller, @@ -9,7 +9,6 @@ import { ParseUUIDPipe, Post, Request, - UseGuards, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; @@ -19,9 +18,9 @@ import { CreateProtectJobDto } from '@/invisible-protect/dto/create-protect-job. import { ProtectJobStatusDto } from '@/invisible-protect/dto/protect-job-status.dto'; import { InvisibleProtectService } from '@/invisible-protect/invisible-protect.service'; +// Auth handled globally via StandaloneAuthModule APP_GUARD @ApiTags('video-studio') @ApiBearerAuth() -@UseGuards(JwtStandaloneGuard) @Controller('video-studio') export class InvisibleProtectController { constructor(private readonly service: InvisibleProtectService) {} diff --git a/features/video-studio/backend-api/src/jobs/jobs.controller.ts b/features/video-studio/backend-api/src/jobs/jobs.controller.ts index 0f8e3b1c9..0c29eff87 100644 --- a/features/video-studio/backend-api/src/jobs/jobs.controller.ts +++ b/features/video-studio/backend-api/src/jobs/jobs.controller.ts @@ -1,5 +1,5 @@ -import { JwtStandaloneGuard, type JwtUserPayload } from '@lilith/nestjs-auth'; -import { Controller, Get, Param, ParseUUIDPipe, Request, UseGuards } from '@nestjs/common'; +import { type JwtUserPayload } from '@lilith/nestjs-auth'; +import { Controller, Get, Param, ParseUUIDPipe, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import type { Request as ExpressRequest } from 'express'; @@ -9,7 +9,6 @@ import { JobsService } from '@/jobs/jobs.service'; @ApiTags('video-studio') @ApiBearerAuth() -@UseGuards(JwtStandaloneGuard) @Controller('video-studio') export class JobsController { constructor(private readonly service: JobsService) {} diff --git a/features/video-studio/backend-api/src/library/library.controller.ts b/features/video-studio/backend-api/src/library/library.controller.ts index 3e08752b6..5cabc0bcc 100644 --- a/features/video-studio/backend-api/src/library/library.controller.ts +++ b/features/video-studio/backend-api/src/library/library.controller.ts @@ -1,5 +1,4 @@ -import { JwtStandaloneGuard } from '@lilith/nestjs-auth'; -import { Controller, Get, UseGuards } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { PhotoItem } from '@/clients/media-gallery/media-gallery.types'; @@ -7,7 +6,6 @@ import { LibraryService } from '@/library/library.service'; @ApiTags('video-studio') @ApiBearerAuth() -@UseGuards(JwtStandaloneGuard) @Controller('video-studio') export class LibraryController { constructor(private readonly service: LibraryService) {}