206 lines
6.2 KiB
TypeScript
206 lines
6.2 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
}
|