import { CACHE_MANAGER } from "@nestjs/cache-manager"; import { BadRequestException, forwardRef, Inject, Injectable, Logger, } from "@nestjs/common"; import type { Cache } from "cache-manager"; import { v4 as uuidv4 } from "uuid"; import { RbacService } from "../auth/rbac.service"; import type { IMediaService } from "../common/interfaces/media.interface"; import type { IStorageService } from "../common/interfaces/storage.interface"; import { MediaService } from "../media/media.service"; import { S3Service } from "../s3/s3.service"; import { UpdateUserDto } from "./dto/update-user.dto"; import { UsersRepository } from "./repositories/users.repository"; @Injectable() export class UsersService { private readonly logger = new Logger(UsersService.name); constructor( private readonly usersRepository: UsersRepository, @Inject(CACHE_MANAGER) private cacheManager: Cache, @Inject(forwardRef(() => RbacService)) private readonly rbacService: RbacService, @Inject(MediaService) private readonly mediaService: IMediaService, @Inject(S3Service) private readonly s3Service: IStorageService, ) {} private async clearUserCache(username?: string) { if (username) { await this.cacheManager.del(`users/profile/${username}`); } } async create(data: { username: string; email: string; passwordHash: string; emailHash: string; }) { return await this.usersRepository.create(data); } async findByEmailHash(emailHash: string) { return await this.usersRepository.findByEmailHash(emailHash); } async findOneWithPrivateData(uuid: string) { const [user, roles] = await Promise.all([ this.usersRepository.findOneWithPrivateData(uuid), this.rbacService.getUserRoles(uuid), ]); if (!user) return null; return { ...user, avatarUrl: user.avatarUrl ? this.s3Service.getPublicUrl(user.avatarUrl) : null, role: roles.includes("admin") ? "admin" : "user", roles, }; } async findAll(limit: number, offset: number) { const [data, totalCount] = await Promise.all([ this.usersRepository.findAll(limit, offset), this.usersRepository.countAll(), ]); const processedData = data.map((user) => ({ ...user, avatarUrl: user.avatarUrl ? this.s3Service.getPublicUrl(user.avatarUrl) : null, })); return { data: processedData, totalCount }; } async findPublicProfile(username: string) { const user = await this.usersRepository.findByUsername(username); if (!user) return null; return { ...user, avatarUrl: user.avatarUrl ? this.s3Service.getPublicUrl(user.avatarUrl) : null, }; } async findOne(uuid: string) { return await this.usersRepository.findOne(uuid); } async update(uuid: string, data: UpdateUserDto) { this.logger.log(`Updating user profile for ${uuid}`); const result = await this.usersRepository.update(uuid, data); if (result[0]) { await this.clearUserCache(result[0].username); } return result; } async updateAvatar(uuid: string, file: Express.Multer.File) { this.logger.log(`Updating avatar for user ${uuid}`); // Validation du format et de la taille const allowedMimeTypes = ["image/png", "image/jpeg", "image/webp"]; if (!allowedMimeTypes.includes(file.mimetype)) { throw new BadRequestException( "Format d'image non supporté. Formats acceptés: png, jpeg, webp.", ); } if (file.size > 2 * 1024 * 1024) { throw new BadRequestException("Image trop volumineuse. Limite: 2 Mo."); } // 1. Scan Antivirus const scanResult = await this.mediaService.scanFile( file.buffer, file.originalname, ); if (scanResult.isInfected) { throw new BadRequestException( `Le fichier est infecté par ${scanResult.virusName}`, ); } // 2. Traitement (WebP + Redimensionnement 512x512) const processed = await this.mediaService.processImage(file.buffer, "webp", { width: 512, height: 512, }); // 3. Upload vers S3 const key = `avatars/${uuid}/${Date.now()}-${uuidv4()}.${processed.extension}`; await this.s3Service.uploadFile(key, processed.buffer, processed.mimeType); this.logger.log(`Avatar uploaded successfully to S3: ${key}`); // 4. Mise à jour de la base de données const user = await this.update(uuid, { avatarUrl: key }); return user[0]; } async updateConsent( uuid: string, termsVersion: string, privacyVersion: string, ) { return await this.usersRepository.update(uuid, { termsVersion, privacyVersion, gdprAcceptedAt: new Date(), }); } async setTwoFactorSecret(uuid: string, secret: string) { return await this.usersRepository.update(uuid, { twoFactorSecret: secret, }); } async toggleTwoFactor(uuid: string, enabled: boolean) { return await this.usersRepository.update(uuid, { isTwoFactorEnabled: enabled, }); } async getTwoFactorSecret(uuid: string): Promise { return await this.usersRepository.getTwoFactorSecret(uuid); } async exportUserData(uuid: string) { const user = await this.findOneWithPrivateData(uuid); if (!user) return null; const [userContents, userFavorites] = await Promise.all([ this.usersRepository.getUserContents(uuid), this.usersRepository.getUserFavorites(uuid), ]); return { profile: user, contents: userContents, favorites: userFavorites, exportedAt: new Date(), }; } async remove(uuid: string) { return await this.usersRepository.softDeleteUserAndContents(uuid); } }