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.
This commit is contained in:
Mathis HERRIOT
2026-01-10 16:31:06 +01:00
parent 9654553940
commit 5a22ad7480
12 changed files with 129 additions and 13 deletions

View File

@@ -1,14 +1,17 @@
import { createHash, randomBytes } from "node:crypto"; import { createHash, randomBytes } from "node:crypto";
import { Injectable } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { DatabaseService } from "../database/database.service"; import { DatabaseService } from "../database/database.service";
import { apiKeys } from "../database/schemas"; import { apiKeys } from "../database/schemas";
@Injectable() @Injectable()
export class ApiKeysService { export class ApiKeysService {
private readonly logger = new Logger(ApiKeysService.name);
constructor(private readonly databaseService: DatabaseService) {} constructor(private readonly databaseService: DatabaseService) {}
async create(userId: string, name: string, expiresAt?: Date) { async create(userId: string, name: string, expiresAt?: Date) {
this.logger.log(`Creating API key for user ${userId}: ${name}`);
const prefix = "mg_live_"; const prefix = "mg_live_";
const randomPart = randomBytes(24).toString("hex"); const randomPart = randomBytes(24).toString("hex");
const key = `${prefix}${randomPart}`; const key = `${prefix}${randomPart}`;
@@ -46,6 +49,7 @@ export class ApiKeysService {
} }
async revoke(userId: string, keyId: string) { async revoke(userId: string, keyId: string) {
this.logger.log(`Revoking API key ${keyId} for user ${userId}`);
return await this.databaseService.db return await this.databaseService.db
.update(apiKeys) .update(apiKeys)
.set({ isActive: false, updatedAt: new Date() }) .set({ isActive: false, updatedAt: new Date() })

View File

@@ -1,5 +1,6 @@
import { Body, Controller, Headers, Post, Req, Res } from "@nestjs/common"; import { Body, Controller, Headers, Post, Req, Res } from "@nestjs/common";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { Throttle } from "@nestjs/throttler";
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import { getIronSession } from "iron-session"; import { getIronSession } from "iron-session";
import { AuthService } from "./auth.service"; import { AuthService } from "./auth.service";
@@ -16,11 +17,13 @@ export class AuthController {
) {} ) {}
@Post("register") @Post("register")
@Throttle({ default: { limit: 5, ttl: 60000 } })
register(@Body() registerDto: RegisterDto) { register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto); return this.authService.register(registerDto);
} }
@Post("login") @Post("login")
@Throttle({ default: { limit: 5, ttl: 60000 } })
async login( async login(
@Body() loginDto: LoginDto, @Body() loginDto: LoginDto,
@Headers("user-agent") userAgent: string, @Headers("user-agent") userAgent: string,
@@ -52,6 +55,7 @@ export class AuthController {
} }
@Post("verify-2fa") @Post("verify-2fa")
@Throttle({ default: { limit: 5, ttl: 60000 } })
async verifyTwoFactor( async verifyTwoFactor(
@Body() verify2faDto: Verify2faDto, @Body() verify2faDto: Verify2faDto,
@Headers("user-agent") userAgent: string, @Headers("user-agent") userAgent: string,

View File

@@ -1,6 +1,7 @@
import { import {
BadRequestException, BadRequestException,
Injectable, Injectable,
Logger,
UnauthorizedException, UnauthorizedException,
} from "@nestjs/common"; } from "@nestjs/common";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
@@ -14,6 +15,8 @@ import { RegisterDto } from "./dto/register.dto";
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor( constructor(
private readonly usersService: UsersService, private readonly usersService: UsersService,
private readonly cryptoService: CryptoService, private readonly cryptoService: CryptoService,
@@ -22,6 +25,7 @@ export class AuthService {
) {} ) {}
async generateTwoFactorSecret(userId: string) { async generateTwoFactorSecret(userId: string) {
this.logger.log(`Generating 2FA secret for user ${userId}`);
const user = await this.usersService.findOne(userId); const user = await this.usersService.findOne(userId);
if (!user) throw new UnauthorizedException(); if (!user) throw new UnauthorizedException();
@@ -42,6 +46,7 @@ export class AuthService {
} }
async enableTwoFactor(userId: string, token: string) { async enableTwoFactor(userId: string, token: string) {
this.logger.log(`Enabling 2FA for user ${userId}`);
const secret = await this.usersService.getTwoFactorSecret(userId); const secret = await this.usersService.getTwoFactorSecret(userId);
if (!secret) { if (!secret) {
throw new BadRequestException("2FA not initiated"); throw new BadRequestException("2FA not initiated");
@@ -57,6 +62,7 @@ export class AuthService {
} }
async disableTwoFactor(userId: string, token: string) { async disableTwoFactor(userId: string, token: string) {
this.logger.log(`Disabling 2FA for user ${userId}`);
const secret = await this.usersService.getTwoFactorSecret(userId); const secret = await this.usersService.getTwoFactorSecret(userId);
if (!secret) { if (!secret) {
throw new BadRequestException("2FA not enabled"); throw new BadRequestException("2FA not enabled");
@@ -72,6 +78,7 @@ export class AuthService {
} }
async register(dto: RegisterDto) { async register(dto: RegisterDto) {
this.logger.log(`Registering new user: ${dto.username}`);
const { username, email, password } = dto; const { username, email, password } = dto;
const passwordHash = await this.cryptoService.hashPassword(password); const passwordHash = await this.cryptoService.hashPassword(password);
@@ -91,6 +98,7 @@ export class AuthService {
} }
async login(dto: LoginDto, userAgent?: string, ip?: string) { async login(dto: LoginDto, userAgent?: string, ip?: string) {
this.logger.log(`Login attempt for email: ${dto.email}`);
const { email, password } = dto; const { email, password } = dto;
const emailHash = await this.cryptoService.hashEmail(email); const emailHash = await this.cryptoService.hashEmail(email);
@@ -141,6 +149,7 @@ export class AuthService {
userAgent?: string, userAgent?: string,
ip?: string, ip?: string,
) { ) {
this.logger.log(`2FA verification attempt for user ${userId}`);
const user = await this.usersService.findOneWithPrivateData(userId); const user = await this.usersService.findOneWithPrivateData(userId);
if (!user || !user.isTwoFactorEnabled) { if (!user || !user.isTwoFactorEnabled) {
throw new UnauthorizedException(); throw new UnauthorizedException();

View File

@@ -6,7 +6,13 @@ import {
Param, Param,
Patch, Patch,
Post, Post,
UseGuards,
UseInterceptors,
} from "@nestjs/common"; } 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 { CategoriesService } from "./categories.service";
import { CreateCategoryDto } from "./dto/create-category.dto"; import { CreateCategoryDto } from "./dto/create-category.dto";
import { UpdateCategoryDto } from "./dto/update-category.dto"; import { UpdateCategoryDto } from "./dto/update-category.dto";
@@ -16,6 +22,9 @@ export class CategoriesController {
constructor(private readonly categoriesService: CategoriesService) {} constructor(private readonly categoriesService: CategoriesService) {}
@Get() @Get()
@UseInterceptors(CacheInterceptor)
@CacheKey("categories/all")
@CacheTTL(3600000) // 1 heure
findAll() { findAll() {
return this.categoriesService.findAll(); return this.categoriesService.findAll();
} }
@@ -25,18 +34,23 @@ export class CategoriesController {
return this.categoriesService.findOne(id); return this.categoriesService.findOne(id);
} }
// Ces routes devraient être protégées par un AdminGuard
@Post() @Post()
@UseGuards(AuthGuard, RolesGuard)
@Roles("admin")
create(@Body() createCategoryDto: CreateCategoryDto) { create(@Body() createCategoryDto: CreateCategoryDto) {
return this.categoriesService.create(createCategoryDto); return this.categoriesService.create(createCategoryDto);
} }
@Patch(":id") @Patch(":id")
@UseGuards(AuthGuard, RolesGuard)
@Roles("admin")
update(@Param("id") id: string, @Body() updateCategoryDto: UpdateCategoryDto) { update(@Param("id") id: string, @Body() updateCategoryDto: UpdateCategoryDto) {
return this.categoriesService.update(id, updateCategoryDto); return this.categoriesService.update(id, updateCategoryDto);
} }
@Delete(":id") @Delete(":id")
@UseGuards(AuthGuard, RolesGuard)
@Roles("admin")
remove(@Param("id") id: string) { remove(@Param("id") id: string) {
return this.categoriesService.remove(id); return this.categoriesService.remove(id);
} }

View File

@@ -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 { eq } from "drizzle-orm";
import { DatabaseService } from "../database/database.service"; import { DatabaseService } from "../database/database.service";
import { categories } from "../database/schemas"; import { categories } from "../database/schemas";
@@ -7,7 +9,17 @@ import { UpdateCategoryDto } from "./dto/update-category.dto";
@Injectable() @Injectable()
export class CategoriesService { 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() { async findAll() {
return await this.databaseService.db return await this.databaseService.db
@@ -27,17 +39,22 @@ export class CategoriesService {
} }
async create(data: CreateCategoryDto) { async create(data: CreateCategoryDto) {
this.logger.log(`Creating category: ${data.name}`);
const slug = data.name const slug = data.name
.toLowerCase() .toLowerCase()
.replace(/ /g, "-") .replace(/ /g, "-")
.replace(/[^\w-]/g, ""); .replace(/[^\w-]/g, "");
return await this.databaseService.db const result = await this.databaseService.db
.insert(categories) .insert(categories)
.values({ ...data, slug }) .values({ ...data, slug })
.returning(); .returning();
await this.clearCategoriesCache();
return result;
} }
async update(id: string, data: UpdateCategoryDto) { async update(id: string, data: UpdateCategoryDto) {
this.logger.log(`Updating category: ${id}`);
const updateData = { const updateData = {
...data, ...data,
updatedAt: new Date(), updatedAt: new Date(),
@@ -48,17 +65,24 @@ export class CategoriesService {
.replace(/[^\w-]/g, "") .replace(/[^\w-]/g, "")
: undefined, : undefined,
}; };
return await this.databaseService.db const result = await this.databaseService.db
.update(categories) .update(categories)
.set(updateData) .set(updateData)
.where(eq(categories.id, id)) .where(eq(categories.id, id))
.returning(); .returning();
await this.clearCategoriesCache();
return result;
} }
async remove(id: string) { async remove(id: string) {
return await this.databaseService.db this.logger.log(`Removing category: ${id}`);
const result = await this.databaseService.db
.delete(categories) .delete(categories)
.where(eq(categories.id, id)) .where(eq(categories.id, id))
.returning(); .returning();
await this.clearCategoriesCache();
return result;
} }
} }

View File

@@ -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 { ConfigService } from "@nestjs/config";
import { import {
and, and,
@@ -27,13 +30,25 @@ import { CreateContentDto } from "./dto/create-content.dto";
@Injectable() @Injectable()
export class ContentsService { export class ContentsService {
private readonly logger = new Logger(ContentsService.name);
constructor( constructor(
private readonly databaseService: DatabaseService, private readonly databaseService: DatabaseService,
private readonly s3Service: S3Service, private readonly s3Service: S3Service,
private readonly mediaService: MediaService, private readonly mediaService: MediaService,
private readonly configService: ConfigService, 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) { async getUploadUrl(userId: string, fileName: string) {
const key = `uploads/${userId}/${Date.now()}-${fileName}`; const key = `uploads/${userId}/${Date.now()}-${fileName}`;
const url = await this.s3Service.getUploadUrl(key); const url = await this.s3Service.getUploadUrl(key);
@@ -50,6 +65,7 @@ export class ContentsService {
tags?: string[]; tags?: string[];
}, },
) { ) {
this.logger.log(`Uploading and processing file for user ${userId}`);
// 0. Validation du format et de la taille // 0. Validation du format et de la taille
const allowedMimeTypes = [ const allowedMimeTypes = [
"image/png", "image/png",
@@ -225,6 +241,7 @@ export class ContentsService {
} }
async create(userId: string, data: CreateContentDto) { async create(userId: string, data: CreateContentDto) {
this.logger.log(`Creating content for user ${userId}: ${data.title}`);
const { tags: tagNames, ...contentData } = data; const { tags: tagNames, ...contentData } = data;
const slug = await this.ensureUniqueSlug(contentData.title); const slug = await this.ensureUniqueSlug(contentData.title);
@@ -264,6 +281,7 @@ export class ContentsService {
} }
} }
await this.clearContentsCache();
return newContent; return newContent;
}); });
} }
@@ -285,11 +303,17 @@ export class ContentsService {
} }
async remove(id: string, userId: string) { 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) .update(contents)
.set({ deletedAt: new Date() }) .set({ deletedAt: new Date() })
.where(and(eq(contents.id, id), eq(contents.userId, userId))) .where(and(eq(contents.id, id), eq(contents.userId, userId)))
.returning(); .returning();
if (result.length > 0) {
await this.clearContentsCache();
}
return result;
} }
async findOne(idOrSlug: string) { async findOne(idOrSlug: string) {

View File

@@ -1,6 +1,7 @@
import { import {
ConflictException, ConflictException,
Injectable, Injectable,
Logger,
NotFoundException, NotFoundException,
} from "@nestjs/common"; } from "@nestjs/common";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
@@ -9,9 +10,12 @@ import { contents, favorites } from "../database/schemas";
@Injectable() @Injectable()
export class FavoritesService { export class FavoritesService {
private readonly logger = new Logger(FavoritesService.name);
constructor(private readonly databaseService: DatabaseService) {} constructor(private readonly databaseService: DatabaseService) {}
async addFavorite(userId: string, contentId: string) { async addFavorite(userId: string, contentId: string) {
this.logger.log(`Adding favorite: user ${userId}, content ${contentId}`);
// Vérifier si le contenu existe // Vérifier si le contenu existe
const content = await this.databaseService.db const content = await this.databaseService.db
.select() .select()
@@ -35,6 +39,7 @@ export class FavoritesService {
} }
async removeFavorite(userId: string, contentId: string) { async removeFavorite(userId: string, contentId: string) {
this.logger.log(`Removing favorite: user ${userId}, content ${contentId}`);
const result = await this.databaseService.db const result = await this.databaseService.db
.delete(favorites) .delete(favorites)
.where(and(eq(favorites.userId, userId), eq(favorites.contentId, contentId))) .where(and(eq(favorites.userId, userId), eq(favorites.contentId, contentId)))

View File

@@ -1,4 +1,4 @@
import { Injectable } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { DatabaseService } from "../database/database.service"; import { DatabaseService } from "../database/database.service";
import { reports } from "../database/schemas"; import { reports } from "../database/schemas";
@@ -6,9 +6,12 @@ import { CreateReportDto } from "./dto/create-report.dto";
@Injectable() @Injectable()
export class ReportsService { export class ReportsService {
private readonly logger = new Logger(ReportsService.name);
constructor(private readonly databaseService: DatabaseService) {} constructor(private readonly databaseService: DatabaseService) {}
async create(reporterId: string, data: CreateReportDto) { async create(reporterId: string, data: CreateReportDto) {
this.logger.log(`Creating report from user ${reporterId}`);
const [newReport] = await this.databaseService.db const [newReport] = await this.databaseService.db
.insert(reports) .insert(reports)
.values({ .values({
@@ -35,6 +38,7 @@ export class ReportsService {
id: string, id: string,
status: "pending" | "reviewed" | "resolved" | "dismissed", status: "pending" | "reviewed" | "resolved" | "dismissed",
) { ) {
this.logger.log(`Updating report ${id} status to ${status}`);
return await this.databaseService.db return await this.databaseService.db
.update(reports) .update(reports)
.set({ status, updatedAt: new Date() }) .set({ status, updatedAt: new Date() })

View File

@@ -4,7 +4,9 @@ import {
Get, Get,
ParseIntPipe, ParseIntPipe,
Query, Query,
UseInterceptors,
} from "@nestjs/common"; } from "@nestjs/common";
import { CacheInterceptor, CacheTTL } from "@nestjs/cache-manager";
import { TagsService } from "./tags.service"; import { TagsService } from "./tags.service";
@Controller("tags") @Controller("tags")
@@ -12,6 +14,8 @@ export class TagsController {
constructor(private readonly tagsService: TagsService) {} constructor(private readonly tagsService: TagsService) {}
@Get() @Get()
@UseInterceptors(CacheInterceptor)
@CacheTTL(300000) // 5 minutes
findAll( findAll(
@Query("limit", new DefaultValuePipe(10), ParseIntPipe) limit: number, @Query("limit", new DefaultValuePipe(10), ParseIntPipe) limit: number,
@Query("offset", new DefaultValuePipe(0), ParseIntPipe) offset: number, @Query("offset", new DefaultValuePipe(0), ParseIntPipe) offset: number,

View File

@@ -1,10 +1,12 @@
import { Injectable } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { desc, eq, ilike, sql } from "drizzle-orm"; import { desc, eq, ilike, sql } from "drizzle-orm";
import { DatabaseService } from "../database/database.service"; import { DatabaseService } from "../database/database.service";
import { contentsToTags, tags } from "../database/schemas"; import { contentsToTags, tags } from "../database/schemas";
@Injectable() @Injectable()
export class TagsService { export class TagsService {
private readonly logger = new Logger(TagsService.name);
constructor(private readonly databaseService: DatabaseService) {} constructor(private readonly databaseService: DatabaseService) {}
async findAll(options: { async findAll(options: {
@@ -13,6 +15,7 @@ export class TagsService {
query?: string; query?: string;
sortBy?: "popular" | "recent"; sortBy?: "popular" | "recent";
}) { }) {
this.logger.log(`Fetching tags with options: ${JSON.stringify(options)}`);
const { limit, offset, query, sortBy } = options; const { limit, offset, query, sortBy } = options;
let whereClause = sql`1=1`; let whereClause = sql`1=1`;

View File

@@ -11,7 +11,9 @@ import {
Query, Query,
Req, Req,
UseGuards, UseGuards,
UseInterceptors,
} from "@nestjs/common"; } from "@nestjs/common";
import { CacheInterceptor, CacheKey, CacheTTL } from "@nestjs/cache-manager";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { Roles } from "../auth/decorators/roles.decorator"; import { Roles } from "../auth/decorators/roles.decorator";
import { AuthGuard } from "../auth/guards/auth.guard"; import { AuthGuard } from "../auth/guards/auth.guard";
@@ -41,6 +43,8 @@ export class UsersController {
// Listing public d'un profil // Listing public d'un profil
@Get("public/:username") @Get("public/:username")
@UseInterceptors(CacheInterceptor)
@CacheTTL(60000) // 1 minute
findPublicProfile(@Param("username") username: string) { findPublicProfile(@Param("username") username: string) {
return this.usersService.findPublicProfile(username); return this.usersService.findPublicProfile(username);
} }

View File

@@ -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 { eq, sql } from "drizzle-orm";
import { CryptoService } from "../crypto/crypto.service"; import { CryptoService } from "../crypto/crypto.service";
import { DatabaseService } from "../database/database.service"; import { DatabaseService } from "../database/database.service";
@@ -11,11 +13,20 @@ import { UpdateUserDto } from "./dto/update-user.dto";
@Injectable() @Injectable()
export class UsersService { export class UsersService {
private readonly logger = new Logger(UsersService.name);
constructor( constructor(
private readonly databaseService: DatabaseService, private readonly databaseService: DatabaseService,
private readonly cryptoService: CryptoService, 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: { async create(data: {
username: string; username: string;
email: string; email: string;
@@ -119,11 +130,17 @@ export class UsersService {
} }
async update(uuid: string, data: UpdateUserDto) { 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) .update(users)
.set({ ...data, updatedAt: new Date() }) .set({ ...data, updatedAt: new Date() })
.where(eq(users.uuid, uuid)) .where(eq(users.uuid, uuid))
.returning(); .returning();
if (result[0]) {
await this.clearUserCache(result[0].username);
}
return result;
} }
async updateConsent( async updateConsent(