import { BadRequestException, Body, Controller, Delete, Get, MaxFileSizeValidator, Param, Patch, Req, Post, ParseFilePipe, 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 { JwtAuthGuard } from '../auth/jwt-auth.guard'; import type { AuthenticatedRequest } from '../auth/jwt-auth.guard'; import { Roles } from '../auth/roles.decorator'; import { RolesGuard } from '../auth/roles.guard'; import { CreateUserDto } from './dto/create-user.dto'; import { AssignUserProjectDto } from './dto/assign-user-project.dto'; import { UpdateMyProfileDto } from './dto/update-my-profile.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { UsersService } from './users.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, `profile-${uniqueSuffix}${extname(file.originalname)}`); }, }); @Controller('users') export class UsersController { private readonly defaultAvatars: string[]; constructor(private readonly usersService: UsersService) { const avatarsDir = join(process.cwd(), 'public', 'default-avatars'); const imageExts = ['.png', '.jpg', '.jpeg', '.webp']; try { this.defaultAvatars = readdirSync(avatarsDir) .filter((f) => imageExts.includes(extname(f).toLowerCase())) .sort() .map((f) => `/default-avatars/${f}`); } catch { this.defaultAvatars = []; } } @Get('default-avatars') listDefaultAvatars() { return this.defaultAvatars; } @Get('me') @UseGuards(JwtAuthGuard) me(@Req() request: AuthenticatedRequest) { return this.usersService.getProfile(request.user!.sub); } @Patch('me') @UseGuards(JwtAuthGuard) updateMe( @Req() request: AuthenticatedRequest, @Body() updateMyProfileDto: UpdateMyProfileDto, ) { return this.usersService.updateMyProfile( request.user!.sub, updateMyProfileDto.displayName, ); } @Post('me/photo') @UseGuards(JwtAuthGuard) @UseInterceptors(FileInterceptor('file', { storage })) uploadPhoto( @Req() request: AuthenticatedRequest, @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.'); } } const profileImageUrl = file ? `/uploads/${file.filename}` : body.bgColor && !file ? undefined : undefined; const bgColor = body.bgColor ?? undefined; if (!file && !bgColor) { throw new BadRequestException('Envoyez une image ou une couleur de fond.'); } return this.usersService.updateProfileImage( request.user!.sub, file ? `/uploads/${file.filename}` : undefined, bgColor, ); } @Delete('me/photo') @UseGuards(JwtAuthGuard) deletePhoto(@Req() request: AuthenticatedRequest) { return this.usersService.updateProfileImage(request.user!.sub, null, null); } @Patch('me/avatar') @UseGuards(JwtAuthGuard) setDefaultAvatar( @Req() request: AuthenticatedRequest, @Body() body: { avatarUrl: string; bgColor?: string }, ) { if (!body.avatarUrl || !this.defaultAvatars.includes(body.avatarUrl)) { throw new BadRequestException('Avatar par defaut invalide.'); } return this.usersService.updateProfileImage( request.user!.sub, body.avatarUrl, body.bgColor, ); } @Post() @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) create(@Req() request: AuthenticatedRequest, @Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto, request.user?.sub); } @Get() @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) list() { return this.usersService.list(); } @Patch(':id') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) update( @Param('id') id: string, @Body() updateUserDto: UpdateUserDto, @Req() request: AuthenticatedRequest, ) { return this.usersService.updateById(id, updateUserDto, request.user?.sub); } @Patch(':id/project') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) assignProject( @Param('id') id: string, @Body() dto: AssignUserProjectDto, @Req() request: AuthenticatedRequest, ) { return this.usersService.assignProjectById(id, dto.projectId, request.user?.sub); } @Delete(':id') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) remove(@Param('id') id: string, @Req() request: AuthenticatedRequest) { return this.usersService.removeById(id, request.user?.sub); } }