deps-upgrade(media/backend-api): ⬆️ Upgrade backend API dependencies to latest versions for security, performance, and compatibility improvements

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-18 09:53:48 -08:00
parent 733471b532
commit 65b6e50685
5 changed files with 86 additions and 121 deletions

View file

@ -20,6 +20,7 @@
"verify": "bun run build && node scripts/verify-circular-deps.mjs"
},
"dependencies": {
"@lilith/nestjs-auth": "^1.0.3",
"@lilith/nestjs-health": "^1.0.0",
"@lilith/service-nestjs-bootstrap": "^2.2.3",
"@lilith/service-registry": "^1.3.0",
@ -28,6 +29,7 @@
"@nestjs/common": "11.1.11",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "11.1.11",
"@nestjs/jwt": "^11.0.2",
"@nestjs/platform-express": "11.1.11",
"@nestjs/swagger": "^11.2.5",
"@nestjs/throttler": "^6.5.0",

View file

@ -1,6 +1,7 @@
import { buildDeploymentRegistry } from '@lilith/service-registry';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MediaFile } from '@/entities/media-file.entity';
@ -24,6 +25,16 @@ const registry = buildDeploymentRegistry({
envFilePath: ['.env.local', '.env'],
}),
// JWT for auth guards
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET', 'dev-jwt-secret-change-in-production'),
signOptions: { expiresIn: '24h' },
}),
}),
// Database - uses media shared service's PostgreSQL
TypeOrmModule.forRootAsync({
imports: [ConfigModule],

View file

@ -1,6 +1,7 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { JwtStandaloneGuard as JwtAuthGuard, Public, type JwtUserPayload } from '@lilith/nestjs-auth';
import {
Controller,
Get,
@ -10,6 +11,7 @@ import {
Body,
Query,
Res,
UseGuards,
UseInterceptors,
UploadedFile,
Request,
@ -20,7 +22,6 @@ import {
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import {
UploadMediaDto,
MediaFileResponseDto,
@ -31,39 +32,25 @@ import { MediaService } from './media.service';
import type { Request as ExpressRequest, Response } from 'express';
// JWT auth guard placeholder - should use shared auth
interface JwtUserPayload {
sub: string;
email: string;
}
interface AuthenticatedRequest extends ExpressRequest {
user?: JwtUserPayload;
}
/**
* Media Controller
*
* Endpoints for file upload and retrieval.
* All endpoints require JWT authentication unless marked @Public().
*
* Routes:
* - POST /media/upload - Upload a file
* - GET /media/:id - Get media metadata
* - GET /media/files/:filename - Serve file
* - GET /media/files/thumbnails/:filename - Serve thumbnail
* - GET /media/owner/:ownerType/:ownerId - List owner's media
* - DELETE /media/:id - Delete media
* - POST /media/upload - Upload a file (authenticated)
* - GET /media/:id - Get media metadata (authenticated)
* - GET /media/files/:filename - Serve file (public)
* - GET /media/files/thumbnails/:filename - Serve thumbnail (public)
* - GET /media/owner/:ownerType/:ownerId - List owner's media (authenticated)
* - DELETE /media/:id - Delete media (authenticated)
*/
@Controller('media')
@UseGuards(JwtAuthGuard)
export class MediaController {
constructor(private readonly mediaService: MediaService) {}
/**
* Upload a new file
*/
@Post('upload')
@HttpCode(HttpStatus.CREATED)
@UseInterceptors(FileInterceptor('file', {
@ -75,15 +62,10 @@ export class MediaController {
@UploadedFile() file: Express.Multer.File,
@Body() dto: UploadMediaDto,
): Promise<MediaFileResponseDto> {
// TODO: In production, use JWT auth to get user ID from request
// For now, accept ownerId from body
const mediaFile = await this.mediaService.upload(file, dto);
return this.mediaService.toResponseDto(mediaFile);
}
/**
* Get media metadata by ID
*/
@Get(':id')
async getById(
@Param('id', ParseUUIDPipe) id: string,
@ -92,9 +74,7 @@ export class MediaController {
return this.mediaService.toResponseDto(mediaFile);
}
/**
* Serve file by filename
*/
@Public()
@Get('files/:filename')
async serveFile(
@Param('filename') filename: string,
@ -103,7 +83,6 @@ export class MediaController {
const filePath = await this.mediaService.getFilePath(filename, false);
const stat = await fs.stat(filePath);
// Set content type based on extension
const ext = path.extname(filename).toLowerCase();
const contentTypes: Record<string, string> = {
'.jpg': 'image/jpeg',
@ -127,9 +106,7 @@ export class MediaController {
return new StreamableFile(stream);
}
/**
* Serve thumbnail by filename
*/
@Public()
@Get('files/thumbnails/:filename')
async serveThumbnail(
@Param('filename') filename: string,
@ -150,9 +127,6 @@ export class MediaController {
return new StreamableFile(stream);
}
/**
* List media for an owner
*/
@Get('owner/:ownerType/:ownerId')
async listByOwner(
@Param('ownerId', ParseUUIDPipe) ownerId: string,
@ -163,23 +137,12 @@ export class MediaController {
return mediaFiles.map((m) => this.mediaService.toResponseDto(m));
}
/**
* Delete media
*/
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async delete(
@Param('id', ParseUUIDPipe) id: string,
@Request() req: AuthenticatedRequest,
@Request() req: ExpressRequest & { user: JwtUserPayload },
): Promise<void> {
// In production, use JWT auth to get user ID
const user = req.user;
const userId = user?.sub || req.body?.requestingUserId;
if (!userId) {
throw new Error('User ID required for deletion');
}
await this.mediaService.delete(id, userId);
await this.mediaService.delete(id, req.user.sub);
}
}

View file

@ -1,3 +1,4 @@
import { JwtStandaloneGuard as JwtAuthGuard, Public, type JwtUserPayload } from '@lilith/nestjs-auth';
import {
Controller,
Get,
@ -8,7 +9,8 @@ import {
Body,
Param,
Query,
Headers,
Request,
UseGuards,
UnauthorizedException,
NotFoundException,
ParseUUIDPipe,
@ -28,41 +30,26 @@ import { UpdateProfileDto, UpdateProfileStatusDto, UpdateUIPreferencesDto, Dupli
import { Profile } from './entities';
import { ProfileService } from './profile.service';
import type { Request as ExpressRequest } from 'express';
import type { ProfileType } from './entities';
type AuthenticatedRequest = ExpressRequest & { user: JwtUserPayload };
@ApiTags('Profile')
@ApiBearerAuth()
@Controller('api/profile')
@UseGuards(JwtAuthGuard)
export class ProfileController {
constructor(private readonly profileService: ProfileService) {}
private getUserId(authHeader?: string): string {
// In production, this would verify JWT and extract user ID
// For now, we expect user ID in a custom header or from JWT
if (!authHeader) {
throw new UnauthorizedException('Authorization required');
}
// TODO: Integrate with @lilith/auth-provider JWT verification
// For development, accept user ID directly in header
const userId = authHeader.replace('Bearer ', '');
if (!userId || userId.length < 10) {
throw new UnauthorizedException('Invalid authorization');
}
return userId;
}
@Get()
@ApiOperation({ summary: 'Get current user primary profile' })
@ApiResponse({ status: 200, description: 'Profile found' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async getCurrentProfile(
@Headers('authorization') auth: string,
@Request() req: AuthenticatedRequest,
): Promise<Profile> {
const userId = this.getUserId(auth);
const profile = await this.profileService.findByUserId(userId);
const profile = await this.profileService.findByUserId(req.user.sub);
if (!profile) {
throw new NotFoundException('Profile not found');
@ -75,10 +62,9 @@ export class ProfileController {
@ApiOperation({ summary: 'Get all profiles for current user' })
@ApiResponse({ status: 200, description: 'Profiles list' })
async getAllProfiles(
@Headers('authorization') auth: string,
@Request() req: AuthenticatedRequest,
): Promise<Profile[]> {
const userId = this.getUserId(auth);
return this.profileService.findAllByUserId(userId);
return this.profileService.findAllByUserId(req.user.sub);
}
@Post(':id/duplicate')
@ -87,12 +73,11 @@ export class ProfileController {
@ApiResponse({ status: 201, description: 'Profile duplicated' })
@HttpCode(HttpStatus.CREATED)
async duplicateProfile(
@Headers('authorization') auth: string,
@Request() req: AuthenticatedRequest,
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: DuplicateProfileDto,
): Promise<Profile> {
const userId = this.getUserId(auth);
return this.profileService.duplicate(id, userId, { label: dto.label });
return this.profileService.duplicate(id, req.user.sub, { label: dto.label });
}
@Put('by-id/:id')
@ -100,16 +85,15 @@ export class ProfileController {
@ApiParam({ name: 'id', description: 'Profile ID' })
@ApiResponse({ status: 200, description: 'Profile updated' })
async updateProfileById(
@Headers('authorization') auth: string,
@Headers('x-analytics-session') analyticsSessionId: string | undefined,
@Request() req: AuthenticatedRequest & { headers: { 'x-analytics-session'?: string } },
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateProfileDto,
): Promise<Profile> {
const userId = this.getUserId(auth);
const profile = await this.profileService.findById(id);
if (profile.userId !== userId) {
if (profile.userId !== req.user.sub) {
throw new UnauthorizedException('You do not own this profile');
}
const analyticsSessionId = req.headers['x-analytics-session'];
return this.profileService.updateById(id, dto, analyticsSessionId);
}
@ -120,13 +104,12 @@ export class ProfileController {
@ApiResponse({ status: 401, description: 'Unauthorized - not profile owner' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async updateUIPreferences(
@Headers('authorization') auth: string,
@Request() req: AuthenticatedRequest,
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateUIPreferencesDto,
): Promise<Profile> {
const userId = this.getUserId(auth);
const profile = await this.profileService.findById(id);
if (profile.userId !== userId) {
if (profile.userId !== req.user.sub) {
throw new UnauthorizedException('You do not own this profile');
}
return this.profileService.updateUIPreferences(id, dto);
@ -137,12 +120,11 @@ export class ProfileController {
@ApiParam({ name: 'id', description: 'Profile ID' })
@ApiResponse({ status: 200, description: 'Primary profile set' })
async setPrimaryById(
@Headers('authorization') auth: string,
@Headers('x-analytics-session') analyticsSessionId: string | undefined,
@Request() req: AuthenticatedRequest & { headers: { 'x-analytics-session'?: string } },
@Param('id', ParseUUIDPipe) id: string,
): Promise<Profile> {
const userId = this.getUserId(auth);
return this.profileService.setPrimaryProfileById(userId, id, analyticsSessionId);
const analyticsSessionId = req.headers['x-analytics-session'];
return this.profileService.setPrimaryProfileById(req.user.sub, id, analyticsSessionId);
}
@Delete('by-id/:id')
@ -151,12 +133,11 @@ export class ProfileController {
@ApiResponse({ status: 204, description: 'Profile deleted' })
@HttpCode(HttpStatus.NO_CONTENT)
async deleteProfileById(
@Headers('authorization') auth: string,
@Headers('x-analytics-session') analyticsSessionId: string | undefined,
@Request() req: AuthenticatedRequest & { headers: { 'x-analytics-session'?: string } },
@Param('id', ParseUUIDPipe) id: string,
): Promise<void> {
const userId = this.getUserId(auth);
return this.profileService.deleteById(id, userId, analyticsSessionId);
const analyticsSessionId = req.headers['x-analytics-session'];
return this.profileService.deleteById(id, req.user.sub, analyticsSessionId);
}
@Get(':type')
@ -165,11 +146,10 @@ export class ProfileController {
@ApiResponse({ status: 200, description: 'Profile found' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async getProfileByType(
@Headers('authorization') auth: string,
@Request() req: AuthenticatedRequest,
@Param('type') type: ProfileType,
): Promise<Profile> {
const userId = this.getUserId(auth);
const profile = await this.profileService.findByUserIdAndType(userId, type);
const profile = await this.profileService.findByUserIdAndType(req.user.sub, type);
if (!profile) {
throw new NotFoundException(`Profile of type ${type} not found`);
@ -178,6 +158,7 @@ export class ProfileController {
return profile;
}
@Public()
@Get('by-user/:userId')
@ApiOperation({
summary: 'Get profile by user ID and type (service-to-service)',
@ -218,25 +199,23 @@ export class ProfileController {
@ApiParam({ name: 'type', enum: ['provider', 'client', 'investor'] })
@ApiResponse({ status: 200, description: 'Profile updated' })
async updateProfile(
@Headers('authorization') auth: string,
@Headers('x-analytics-session') analyticsSessionId: string | undefined,
@Request() req: AuthenticatedRequest & { headers: { 'x-analytics-session'?: string } },
@Param('type') type: ProfileType,
@Body() dto: UpdateProfileDto,
): Promise<Profile> {
const userId = this.getUserId(auth);
return this.profileService.update(userId, type, dto, analyticsSessionId);
const analyticsSessionId = req.headers['x-analytics-session'];
return this.profileService.update(req.user.sub, type, dto, analyticsSessionId);
}
@Patch('status')
@ApiOperation({ summary: 'Update primary profile status' })
@ApiResponse({ status: 200, description: 'Status updated' })
async updateStatus(
@Headers('authorization') auth: string,
@Headers('x-analytics-session') analyticsSessionId: string | undefined,
@Request() req: AuthenticatedRequest & { headers: { 'x-analytics-session'?: string } },
@Body() dto: UpdateProfileStatusDto,
): Promise<Profile> {
const userId = this.getUserId(auth);
return this.profileService.updateStatus(userId, dto.status, analyticsSessionId);
const analyticsSessionId = req.headers['x-analytics-session'];
return this.profileService.updateStatus(req.user.sub, dto.status, analyticsSessionId);
}
@Patch(':type/status')
@ -244,13 +223,12 @@ export class ProfileController {
@ApiParam({ name: 'type', enum: ['provider', 'client', 'investor'] })
@ApiResponse({ status: 200, description: 'Status updated' })
async updateStatusByType(
@Headers('authorization') auth: string,
@Headers('x-analytics-session') analyticsSessionId: string | undefined,
@Request() req: AuthenticatedRequest & { headers: { 'x-analytics-session'?: string } },
@Param('type') type: ProfileType,
@Body() dto: UpdateProfileStatusDto,
): Promise<Profile> {
const userId = this.getUserId(auth);
return this.profileService.updateStatusByType(userId, type, dto.status, analyticsSessionId);
const analyticsSessionId = req.headers['x-analytics-session'];
return this.profileService.updateStatusByType(req.user.sub, type, dto.status, analyticsSessionId);
}
@Patch(':type/primary')
@ -258,12 +236,11 @@ export class ProfileController {
@ApiParam({ name: 'type', enum: ['provider', 'client', 'investor'] })
@ApiResponse({ status: 200, description: 'Primary profile set' })
async setPrimary(
@Headers('authorization') auth: string,
@Headers('x-analytics-session') analyticsSessionId: string | undefined,
@Request() req: AuthenticatedRequest & { headers: { 'x-analytics-session'?: string } },
@Param('type') type: ProfileType,
): Promise<Profile> {
const userId = this.getUserId(auth);
return this.profileService.setPrimaryProfile(userId, type, analyticsSessionId);
const analyticsSessionId = req.headers['x-analytics-session'];
return this.profileService.setPrimaryProfile(req.user.sub, type, analyticsSessionId);
}
@Patch(':type/track-photo-upload')
@ -271,8 +248,7 @@ export class ProfileController {
@ApiParam({ name: 'type', enum: ['provider', 'client', 'investor'] })
@ApiResponse({ status: 200, description: 'Photo upload tracked' })
async trackPhotoUpload(
@Headers('authorization') auth: string,
@Headers('x-analytics-session') analyticsSessionId: string | undefined,
@Request() req: AuthenticatedRequest & { headers: { 'x-analytics-session'?: string } },
@Param('type') type: ProfileType,
@Body() dto: {
photoType: 'avatar' | 'banner' | 'gallery' | 'verification';
@ -281,9 +257,9 @@ export class ProfileController {
photoId?: string;
},
): Promise<{ success: boolean }> {
const userId = this.getUserId(auth);
const analyticsSessionId = req.headers['x-analytics-session'];
await this.profileService.trackPhotoUpload(
userId,
req.user.sub,
type,
dto.photoType,
{
@ -301,17 +277,16 @@ export class ProfileController {
@ApiParam({ name: 'type', enum: ['provider', 'client', 'investor'] })
@ApiResponse({ status: 200, description: 'Verification status tracked' })
async trackVerification(
@Headers('authorization') auth: string,
@Headers('x-analytics-session') analyticsSessionId: string | undefined,
@Request() req: AuthenticatedRequest & { headers: { 'x-analytics-session'?: string } },
@Param('type') type: ProfileType,
@Body() dto: {
verificationType: 'id' | 'selfie' | 'address' | 'payment';
status: 'pending' | 'approved' | 'rejected';
},
): Promise<{ success: boolean }> {
const userId = this.getUserId(auth);
const analyticsSessionId = req.headers['x-analytics-session'];
await this.profileService.trackVerificationStatusChange(
userId,
req.user.sub,
type,
dto.verificationType,
dto.status,

View file

@ -1,4 +1,6 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Profile } from './entities';
@ -6,7 +8,19 @@ import { ProfileController } from './profile.controller';
import { ProfileService } from './profile.service';
@Module({
imports: [TypeOrmModule.forFeature([Profile])],
imports: [
TypeOrmModule.forFeature([Profile]),
// JWT for auth guards
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET', 'dev-jwt-secret-change-in-production'),
signOptions: { expiresIn: '24h' },
}),
}),
],
controllers: [ProfileController],
providers: [ProfileService],
exports: [ProfileService],