import { BadRequestException, Body, Controller, Delete, Get, MaxFileSizeValidator, Param, Patch, Post, ParseFilePipe, Req, UploadedFile, UseGuards, UseInterceptors, } from '@nestjs/common'; import { UserRole } from '@prisma/client'; import { FileInterceptor } from '@nestjs/platform-express'; import { diskStorage } from 'multer'; import { extname, join } from 'path'; import { existsSync, mkdirSync, readdirSync } from 'fs'; import type { AuthenticatedRequest } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { Roles } from '../auth/roles.decorator'; import { RolesGuard } from '../auth/roles.guard'; import { AssignProjectParticipantDto } from './dto/assign-project-participant.dto'; import { CloneProjectDto } from './dto/clone-project.dto'; import { CreateProjectDto } from './dto/create-project.dto'; import { UpdateProjectDto } from './dto/update-project.dto'; import { ProjectsService } from './projects.service'; const uploadsDir = join(process.cwd(), 'uploads'); const storage = diskStorage({ destination: (_req, _file, callback) => { if (!existsSync(uploadsDir)) { mkdirSync(uploadsDir, { recursive: true }); } callback(null, uploadsDir); }, filename: (_req, file, callback) => { const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`; callback(null, `project-${uniqueSuffix}${extname(file.originalname)}`); }, }); @Controller('projects') @UseGuards(JwtAuthGuard) export class ProjectsController { private readonly defaultProjectAvatars: string[]; constructor(private readonly projectsService: ProjectsService) { const avatarsDir = join(process.cwd(), 'public', 'default-project-avatars'); const imageExts = ['.png', '.jpg', '.jpeg', '.webp']; try { this.defaultProjectAvatars = readdirSync(avatarsDir) .filter((f) => imageExts.includes(extname(f).toLowerCase())) .sort() .map((f) => `/default-project-avatars/${f}`); } catch { this.defaultProjectAvatars = []; } } @Get() listForUser(@Req() request: AuthenticatedRequest) { return this.projectsService.listForUser(request.user!.sub); } @Get(':projectId') getById(@Req() request: AuthenticatedRequest, @Param('projectId') projectId: string) { return this.projectsService.getByIdForUser(request.user!.sub, projectId); } @Post() @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) create(@Req() request: AuthenticatedRequest, @Body() dto: CreateProjectDto) { return this.projectsService.create(request.user!.sub, dto); } @Patch(':projectId') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) update( @Req() request: AuthenticatedRequest, @Param('projectId') projectId: string, @Body() dto: UpdateProjectDto, ) { return this.projectsService.update(request.user!.sub, projectId, dto); } @Post(':projectId/clone') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) clone( @Req() request: AuthenticatedRequest, @Param('projectId') projectId: string, @Body() dto: CloneProjectDto, ) { return this.projectsService.clone(request.user!.sub, projectId, dto); } @Get(':projectId/participants') listParticipants(@Req() request: AuthenticatedRequest, @Param('projectId') projectId: string) { return this.projectsService.listParticipants(request.user!.sub, projectId); } @Post(':projectId/participants') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) addParticipant( @Req() request: AuthenticatedRequest, @Param('projectId') projectId: string, @Body() dto: AssignProjectParticipantDto, ) { return this.projectsService.addParticipant(request.user!.sub, projectId, dto); } @Delete(':projectId/participants/:participantUserId') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) removeParticipant( @Req() request: AuthenticatedRequest, @Param('projectId') projectId: string, @Param('participantUserId') participantUserId: string, ) { return this.projectsService.removeParticipant( request.user!.sub, projectId, participantUserId, ); } @Get(':projectId/default-project-avatars') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) listDefaultProjectAvatars() { return this.defaultProjectAvatars; } @Post(':projectId/photo') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) @UseInterceptors(FileInterceptor('file', { storage })) uploadProjectPhoto( @Req() request: AuthenticatedRequest, @Param('projectId') projectId: string, @Body() body: { bgColor?: string }, @UploadedFile( new ParseFilePipe({ validators: [new MaxFileSizeValidator({ maxSize: 5 * 1024 * 1024 })], fileIsRequired: false, }), ) file?: Express.Multer.File, ) { if (file) { const allowedMimeTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp']; if (!allowedMimeTypes.includes(file.mimetype)) { throw new BadRequestException('Format non supporté. Utilisez PNG, JPG ou WEBP.'); } } if (!file && !body.bgColor) { throw new BadRequestException('Envoyez une image ou une couleur de fond.'); } return this.projectsService.updateProjectImage( request.user!.sub, projectId, file ? `/uploads/${file.filename}` : undefined, body.bgColor ?? undefined, ); } @Delete(':projectId/photo') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) deleteProjectPhoto( @Req() request: AuthenticatedRequest, @Param('projectId') projectId: string, ) { return this.projectsService.updateProjectImage(request.user!.sub, projectId, null, null); } @Patch(':projectId/avatar') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) setProjectDefaultAvatar( @Req() request: AuthenticatedRequest, @Param('projectId') projectId: string, @Body() body: { avatarUrl: string; bgColor?: string }, ) { if (!body.avatarUrl || !this.defaultProjectAvatars.includes(body.avatarUrl)) { throw new BadRequestException('Avatar par defaut invalide.'); } return this.projectsService.updateProjectImage( request.user!.sub, projectId, body.avatarUrl, body.bgColor, ); } }