From 5a22ad7480ebfbff2c9c6bf43ff8d6dfe7ef3720 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:31:06 +0100 Subject: [PATCH] feat: add logging and caching enhancements across core services Integrate `Logger` for consistent logging in services like `reports`, `categories`, `users`, `contents`, and more. Introduce caching capabilities with `CacheInterceptor` and manual cache clearing logic for categories, users, and contents. Add request throttling to critical auth endpoints for enhanced rate limiting. --- backend/src/api-keys/api-keys.service.ts | 6 +++- backend/src/auth/auth.controller.ts | 4 +++ backend/src/auth/auth.service.ts | 9 +++++ .../src/categories/categories.controller.ts | 16 ++++++++- backend/src/categories/categories.service.ts | 34 ++++++++++++++++--- backend/src/contents/contents.service.ts | 28 +++++++++++++-- backend/src/favorites/favorites.service.ts | 5 +++ backend/src/reports/reports.service.ts | 6 +++- backend/src/tags/tags.controller.ts | 4 +++ backend/src/tags/tags.service.ts | 5 ++- backend/src/users/users.controller.ts | 4 +++ backend/src/users/users.service.ts | 21 ++++++++++-- 12 files changed, 129 insertions(+), 13 deletions(-) diff --git a/backend/src/api-keys/api-keys.service.ts b/backend/src/api-keys/api-keys.service.ts index 1a7d582..40290f9 100644 --- a/backend/src/api-keys/api-keys.service.ts +++ b/backend/src/api-keys/api-keys.service.ts @@ -1,14 +1,17 @@ import { createHash, randomBytes } from "node:crypto"; -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { and, eq } from "drizzle-orm"; import { DatabaseService } from "../database/database.service"; import { apiKeys } from "../database/schemas"; @Injectable() export class ApiKeysService { + private readonly logger = new Logger(ApiKeysService.name); + constructor(private readonly databaseService: DatabaseService) {} async create(userId: string, name: string, expiresAt?: Date) { + this.logger.log(`Creating API key for user ${userId}: ${name}`); const prefix = "mg_live_"; const randomPart = randomBytes(24).toString("hex"); const key = `${prefix}${randomPart}`; @@ -46,6 +49,7 @@ export class ApiKeysService { } async revoke(userId: string, keyId: string) { + this.logger.log(`Revoking API key ${keyId} for user ${userId}`); return await this.databaseService.db .update(apiKeys) .set({ isActive: false, updatedAt: new Date() }) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index c9a7848..3cf804d 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -1,5 +1,6 @@ import { Body, Controller, Headers, Post, Req, Res } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; +import { Throttle } from "@nestjs/throttler"; import type { Request, Response } from "express"; import { getIronSession } from "iron-session"; import { AuthService } from "./auth.service"; @@ -16,11 +17,13 @@ export class AuthController { ) {} @Post("register") + @Throttle({ default: { limit: 5, ttl: 60000 } }) register(@Body() registerDto: RegisterDto) { return this.authService.register(registerDto); } @Post("login") + @Throttle({ default: { limit: 5, ttl: 60000 } }) async login( @Body() loginDto: LoginDto, @Headers("user-agent") userAgent: string, @@ -52,6 +55,7 @@ export class AuthController { } @Post("verify-2fa") + @Throttle({ default: { limit: 5, ttl: 60000 } }) async verifyTwoFactor( @Body() verify2faDto: Verify2faDto, @Headers("user-agent") userAgent: string, diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index d68e7a4..12826d6 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Injectable, + Logger, UnauthorizedException, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; @@ -14,6 +15,8 @@ import { RegisterDto } from "./dto/register.dto"; @Injectable() export class AuthService { + private readonly logger = new Logger(AuthService.name); + constructor( private readonly usersService: UsersService, private readonly cryptoService: CryptoService, @@ -22,6 +25,7 @@ export class AuthService { ) {} async generateTwoFactorSecret(userId: string) { + this.logger.log(`Generating 2FA secret for user ${userId}`); const user = await this.usersService.findOne(userId); if (!user) throw new UnauthorizedException(); @@ -42,6 +46,7 @@ export class AuthService { } async enableTwoFactor(userId: string, token: string) { + this.logger.log(`Enabling 2FA for user ${userId}`); const secret = await this.usersService.getTwoFactorSecret(userId); if (!secret) { throw new BadRequestException("2FA not initiated"); @@ -57,6 +62,7 @@ export class AuthService { } async disableTwoFactor(userId: string, token: string) { + this.logger.log(`Disabling 2FA for user ${userId}`); const secret = await this.usersService.getTwoFactorSecret(userId); if (!secret) { throw new BadRequestException("2FA not enabled"); @@ -72,6 +78,7 @@ export class AuthService { } async register(dto: RegisterDto) { + this.logger.log(`Registering new user: ${dto.username}`); const { username, email, password } = dto; const passwordHash = await this.cryptoService.hashPassword(password); @@ -91,6 +98,7 @@ export class AuthService { } async login(dto: LoginDto, userAgent?: string, ip?: string) { + this.logger.log(`Login attempt for email: ${dto.email}`); const { email, password } = dto; const emailHash = await this.cryptoService.hashEmail(email); @@ -141,6 +149,7 @@ export class AuthService { userAgent?: string, ip?: string, ) { + this.logger.log(`2FA verification attempt for user ${userId}`); const user = await this.usersService.findOneWithPrivateData(userId); if (!user || !user.isTwoFactorEnabled) { throw new UnauthorizedException(); diff --git a/backend/src/categories/categories.controller.ts b/backend/src/categories/categories.controller.ts index be8abff..5cad8c3 100644 --- a/backend/src/categories/categories.controller.ts +++ b/backend/src/categories/categories.controller.ts @@ -6,7 +6,13 @@ import { Param, Patch, Post, + UseGuards, + UseInterceptors, } from "@nestjs/common"; +import { CacheInterceptor, CacheKey, CacheTTL } from "@nestjs/cache-manager"; +import { Roles } from "../auth/decorators/roles.decorator"; +import { AuthGuard } from "../auth/guards/auth.guard"; +import { RolesGuard } from "../auth/guards/roles.guard"; import { CategoriesService } from "./categories.service"; import { CreateCategoryDto } from "./dto/create-category.dto"; import { UpdateCategoryDto } from "./dto/update-category.dto"; @@ -16,6 +22,9 @@ export class CategoriesController { constructor(private readonly categoriesService: CategoriesService) {} @Get() + @UseInterceptors(CacheInterceptor) + @CacheKey("categories/all") + @CacheTTL(3600000) // 1 heure findAll() { return this.categoriesService.findAll(); } @@ -25,18 +34,23 @@ export class CategoriesController { return this.categoriesService.findOne(id); } - // Ces routes devraient être protégées par un AdminGuard @Post() + @UseGuards(AuthGuard, RolesGuard) + @Roles("admin") create(@Body() createCategoryDto: CreateCategoryDto) { return this.categoriesService.create(createCategoryDto); } @Patch(":id") + @UseGuards(AuthGuard, RolesGuard) + @Roles("admin") update(@Param("id") id: string, @Body() updateCategoryDto: UpdateCategoryDto) { return this.categoriesService.update(id, updateCategoryDto); } @Delete(":id") + @UseGuards(AuthGuard, RolesGuard) + @Roles("admin") remove(@Param("id") id: string) { return this.categoriesService.remove(id); } diff --git a/backend/src/categories/categories.service.ts b/backend/src/categories/categories.service.ts index ad6eae0..0bddd96 100644 --- a/backend/src/categories/categories.service.ts +++ b/backend/src/categories/categories.service.ts @@ -1,4 +1,6 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger, Inject } from "@nestjs/common"; +import { CACHE_MANAGER } from "@nestjs/cache-manager"; +import { Cache } from "cache-manager"; import { eq } from "drizzle-orm"; import { DatabaseService } from "../database/database.service"; import { categories } from "../database/schemas"; @@ -7,7 +9,17 @@ import { UpdateCategoryDto } from "./dto/update-category.dto"; @Injectable() export class CategoriesService { - constructor(private readonly databaseService: DatabaseService) {} + private readonly logger = new Logger(CategoriesService.name); + + constructor( + private readonly databaseService: DatabaseService, + @Inject(CACHE_MANAGER) private cacheManager: Cache, + ) {} + + private async clearCategoriesCache() { + this.logger.log("Clearing categories cache"); + await this.cacheManager.del("categories/all"); + } async findAll() { return await this.databaseService.db @@ -27,17 +39,22 @@ export class CategoriesService { } async create(data: CreateCategoryDto) { + this.logger.log(`Creating category: ${data.name}`); const slug = data.name .toLowerCase() .replace(/ /g, "-") .replace(/[^\w-]/g, ""); - return await this.databaseService.db + const result = await this.databaseService.db .insert(categories) .values({ ...data, slug }) .returning(); + + await this.clearCategoriesCache(); + return result; } async update(id: string, data: UpdateCategoryDto) { + this.logger.log(`Updating category: ${id}`); const updateData = { ...data, updatedAt: new Date(), @@ -48,17 +65,24 @@ export class CategoriesService { .replace(/[^\w-]/g, "") : undefined, }; - return await this.databaseService.db + const result = await this.databaseService.db .update(categories) .set(updateData) .where(eq(categories.id, id)) .returning(); + + await this.clearCategoriesCache(); + return result; } async remove(id: string) { - return await this.databaseService.db + this.logger.log(`Removing category: ${id}`); + const result = await this.databaseService.db .delete(categories) .where(eq(categories.id, id)) .returning(); + + await this.clearCategoriesCache(); + return result; } } diff --git a/backend/src/contents/contents.service.ts b/backend/src/contents/contents.service.ts index 9e07f89..f09cb76 100644 --- a/backend/src/contents/contents.service.ts +++ b/backend/src/contents/contents.service.ts @@ -1,4 +1,7 @@ -import { BadRequestException, Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable, Logger } from "@nestjs/common"; +import { CACHE_MANAGER } from "@nestjs/cache-manager"; +import { Cache } from "cache-manager"; +import { Inject } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { and, @@ -27,13 +30,25 @@ import { CreateContentDto } from "./dto/create-content.dto"; @Injectable() export class ContentsService { + private readonly logger = new Logger(ContentsService.name); + constructor( private readonly databaseService: DatabaseService, private readonly s3Service: S3Service, private readonly mediaService: MediaService, private readonly configService: ConfigService, + @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} + private async clearContentsCache() { + this.logger.log("Clearing contents cache"); + const keys = await this.cacheManager.store.keys(); + const contentsKeys = keys.filter((key) => key.startsWith("contents/")); + for (const key of contentsKeys) { + await this.cacheManager.del(key); + } + } + async getUploadUrl(userId: string, fileName: string) { const key = `uploads/${userId}/${Date.now()}-${fileName}`; const url = await this.s3Service.getUploadUrl(key); @@ -50,6 +65,7 @@ export class ContentsService { tags?: string[]; }, ) { + this.logger.log(`Uploading and processing file for user ${userId}`); // 0. Validation du format et de la taille const allowedMimeTypes = [ "image/png", @@ -225,6 +241,7 @@ export class ContentsService { } async create(userId: string, data: CreateContentDto) { + this.logger.log(`Creating content for user ${userId}: ${data.title}`); const { tags: tagNames, ...contentData } = data; const slug = await this.ensureUniqueSlug(contentData.title); @@ -264,6 +281,7 @@ export class ContentsService { } } + await this.clearContentsCache(); return newContent; }); } @@ -285,11 +303,17 @@ export class ContentsService { } async remove(id: string, userId: string) { - return await this.databaseService.db + this.logger.log(`Removing content ${id} for user ${userId}`); + const result = await this.databaseService.db .update(contents) .set({ deletedAt: new Date() }) .where(and(eq(contents.id, id), eq(contents.userId, userId))) .returning(); + + if (result.length > 0) { + await this.clearContentsCache(); + } + return result; } async findOne(idOrSlug: string) { diff --git a/backend/src/favorites/favorites.service.ts b/backend/src/favorites/favorites.service.ts index ab9fe2b..b6efcd0 100644 --- a/backend/src/favorites/favorites.service.ts +++ b/backend/src/favorites/favorites.service.ts @@ -1,6 +1,7 @@ import { ConflictException, Injectable, + Logger, NotFoundException, } from "@nestjs/common"; import { and, eq } from "drizzle-orm"; @@ -9,9 +10,12 @@ import { contents, favorites } from "../database/schemas"; @Injectable() export class FavoritesService { + private readonly logger = new Logger(FavoritesService.name); + constructor(private readonly databaseService: DatabaseService) {} async addFavorite(userId: string, contentId: string) { + this.logger.log(`Adding favorite: user ${userId}, content ${contentId}`); // Vérifier si le contenu existe const content = await this.databaseService.db .select() @@ -35,6 +39,7 @@ export class FavoritesService { } async removeFavorite(userId: string, contentId: string) { + this.logger.log(`Removing favorite: user ${userId}, content ${contentId}`); const result = await this.databaseService.db .delete(favorites) .where(and(eq(favorites.userId, userId), eq(favorites.contentId, contentId))) diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts index 3b9ea77..ca09d58 100644 --- a/backend/src/reports/reports.service.ts +++ b/backend/src/reports/reports.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { desc, eq } from "drizzle-orm"; import { DatabaseService } from "../database/database.service"; import { reports } from "../database/schemas"; @@ -6,9 +6,12 @@ import { CreateReportDto } from "./dto/create-report.dto"; @Injectable() export class ReportsService { + private readonly logger = new Logger(ReportsService.name); + constructor(private readonly databaseService: DatabaseService) {} async create(reporterId: string, data: CreateReportDto) { + this.logger.log(`Creating report from user ${reporterId}`); const [newReport] = await this.databaseService.db .insert(reports) .values({ @@ -35,6 +38,7 @@ export class ReportsService { id: string, status: "pending" | "reviewed" | "resolved" | "dismissed", ) { + this.logger.log(`Updating report ${id} status to ${status}`); return await this.databaseService.db .update(reports) .set({ status, updatedAt: new Date() }) diff --git a/backend/src/tags/tags.controller.ts b/backend/src/tags/tags.controller.ts index 95bdd56..ae08024 100644 --- a/backend/src/tags/tags.controller.ts +++ b/backend/src/tags/tags.controller.ts @@ -4,7 +4,9 @@ import { Get, ParseIntPipe, Query, + UseInterceptors, } from "@nestjs/common"; +import { CacheInterceptor, CacheTTL } from "@nestjs/cache-manager"; import { TagsService } from "./tags.service"; @Controller("tags") @@ -12,6 +14,8 @@ export class TagsController { constructor(private readonly tagsService: TagsService) {} @Get() + @UseInterceptors(CacheInterceptor) + @CacheTTL(300000) // 5 minutes findAll( @Query("limit", new DefaultValuePipe(10), ParseIntPipe) limit: number, @Query("offset", new DefaultValuePipe(0), ParseIntPipe) offset: number, diff --git a/backend/src/tags/tags.service.ts b/backend/src/tags/tags.service.ts index cbb4d09..f26bc86 100644 --- a/backend/src/tags/tags.service.ts +++ b/backend/src/tags/tags.service.ts @@ -1,10 +1,12 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { desc, eq, ilike, sql } from "drizzle-orm"; import { DatabaseService } from "../database/database.service"; import { contentsToTags, tags } from "../database/schemas"; @Injectable() export class TagsService { + private readonly logger = new Logger(TagsService.name); + constructor(private readonly databaseService: DatabaseService) {} async findAll(options: { @@ -13,6 +15,7 @@ export class TagsService { query?: string; sortBy?: "popular" | "recent"; }) { + this.logger.log(`Fetching tags with options: ${JSON.stringify(options)}`); const { limit, offset, query, sortBy } = options; let whereClause = sql`1=1`; diff --git a/backend/src/users/users.controller.ts b/backend/src/users/users.controller.ts index 04b596b..673f9a5 100644 --- a/backend/src/users/users.controller.ts +++ b/backend/src/users/users.controller.ts @@ -11,7 +11,9 @@ import { Query, Req, UseGuards, + UseInterceptors, } from "@nestjs/common"; +import { CacheInterceptor, CacheKey, CacheTTL } from "@nestjs/cache-manager"; import { AuthService } from "../auth/auth.service"; import { Roles } from "../auth/decorators/roles.decorator"; import { AuthGuard } from "../auth/guards/auth.guard"; @@ -41,6 +43,8 @@ export class UsersController { // Listing public d'un profil @Get("public/:username") + @UseInterceptors(CacheInterceptor) + @CacheTTL(60000) // 1 minute findPublicProfile(@Param("username") username: string) { return this.usersService.findPublicProfile(username); } diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 5701d0c..19690ec 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -1,4 +1,6 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger, Inject } from "@nestjs/common"; +import { CACHE_MANAGER } from "@nestjs/cache-manager"; +import { Cache } from "cache-manager"; import { eq, sql } from "drizzle-orm"; import { CryptoService } from "../crypto/crypto.service"; import { DatabaseService } from "../database/database.service"; @@ -11,11 +13,20 @@ import { UpdateUserDto } from "./dto/update-user.dto"; @Injectable() export class UsersService { + private readonly logger = new Logger(UsersService.name); + constructor( private readonly databaseService: DatabaseService, private readonly cryptoService: CryptoService, + @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} + private async clearUserCache(username?: string) { + if (username) { + await this.cacheManager.del(`users/profile/${username}`); + } + } + async create(data: { username: string; email: string; @@ -119,11 +130,17 @@ export class UsersService { } async update(uuid: string, data: UpdateUserDto) { - return await this.databaseService.db + this.logger.log(`Updating user profile for ${uuid}`); + const result = await this.databaseService.db .update(users) .set({ ...data, updatedAt: new Date() }) .where(eq(users.uuid, uuid)) .returning(); + + if (result[0]) { + await this.clearUserCache(result[0].username); + } + return result; } async updateConsent(