Files
memegoat/backend/src/users/users.service.ts
Mathis HERRIOT 8d27532dc0
Some checks failed
Backend Tests / test (push) Successful in 1m11s
Lint / lint (backend) (push) Failing after 46s
Lint / lint (documentation) (push) Successful in 1m7s
Lint / lint (frontend) (push) Has been cancelled
feat(s3): enhance logging and public URL generation
Add detailed logging for S3 uploads in user and content services. Improve public URL generation logic in `S3Service` by providing better handling for `API_URL`, `DOMAIN_NAME`, and `PORT`. Update relevant tests to cover all scenarios.
2026-01-15 00:40:36 +01:00

202 lines
5.3 KiB
TypeScript

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<string | null> {
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);
}
}