From 2218768adb6ce56e833aa83a07947311ce17b059 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:25:04 +0100 Subject: [PATCH] feat: add CommonModule with PurgeService and global exception filter Introduced CommonModule to centralize shared functionality. Added PurgeService for automated database cleanup and a global exception filter for unified error handling. --- backend/src/common/common.module.ts | 11 ++++ .../common/filters/http-exception.filter.ts | 56 +++++++++++++++++ .../common/interfaces/request.interface.ts | 8 +++ backend/src/common/services/purge.service.ts | 63 +++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 backend/src/common/common.module.ts create mode 100644 backend/src/common/filters/http-exception.filter.ts create mode 100644 backend/src/common/interfaces/request.interface.ts create mode 100644 backend/src/common/services/purge.service.ts diff --git a/backend/src/common/common.module.ts b/backend/src/common/common.module.ts new file mode 100644 index 0000000..83f544c --- /dev/null +++ b/backend/src/common/common.module.ts @@ -0,0 +1,11 @@ +import { Global, Module } from "@nestjs/common"; +import { DatabaseModule } from "../database/database.module"; +import { PurgeService } from "./services/purge.service"; + +@Global() +@Module({ + imports: [DatabaseModule], + providers: [PurgeService], + exports: [PurgeService], +}) +export class CommonModule {} diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts new file mode 100644 index 0000000..6ef6e4f --- /dev/null +++ b/backend/src/common/filters/http-exception.filter.ts @@ -0,0 +1,56 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Logger, +} from "@nestjs/common"; +import * as Sentry from "@sentry/nestjs"; +import { Request, Response } from "express"; + +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + private readonly logger = new Logger("ExceptionFilter"); + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.getResponse() + : "Internal server error"; + + const errorResponse = { + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + method: request.method, + message: + typeof message === "object" && message !== null + ? (message as Record).message || message + : message, + }; + + if (status === HttpStatus.INTERNAL_SERVER_ERROR) { + Sentry.captureException(exception); + this.logger.error( + `${request.method} ${request.url} - Error: ${exception instanceof Error ? exception.message : "Unknown error"}`, + exception instanceof Error ? exception.stack : "", + ); + } else { + this.logger.warn( + `${request.method} ${request.url} - Status: ${status} - Message: ${JSON.stringify(message)}`, + ); + } + + response.status(status).json(errorResponse); + } +} diff --git a/backend/src/common/interfaces/request.interface.ts b/backend/src/common/interfaces/request.interface.ts new file mode 100644 index 0000000..3232547 --- /dev/null +++ b/backend/src/common/interfaces/request.interface.ts @@ -0,0 +1,8 @@ +import { Request } from "express"; + +export interface AuthenticatedRequest extends Request { + user: { + sub: string; + username: string; + }; +} diff --git a/backend/src/common/services/purge.service.ts b/backend/src/common/services/purge.service.ts new file mode 100644 index 0000000..504dfe5 --- /dev/null +++ b/backend/src/common/services/purge.service.ts @@ -0,0 +1,63 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { Cron, CronExpression } from "@nestjs/schedule"; +import { and, eq, isNotNull, lte } from "drizzle-orm"; +import { DatabaseService } from "../../database/database.service"; +import { contents, reports, sessions, users } from "../../database/schemas"; + +@Injectable() +export class PurgeService { + private readonly logger = new Logger(PurgeService.name); + + constructor(private readonly databaseService: DatabaseService) {} + + // Toutes les nuits à minuit + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async purgeExpiredData() { + this.logger.log("Starting automatic data purge..."); + + try { + const now = new Date(); + + // 1. Purge des sessions expirées + const deletedSessions = await this.databaseService.db + .delete(sessions) + .where(lte(sessions.expiresAt, now)) + .returning(); + this.logger.log(`Purged ${deletedSessions.length} expired sessions.`); + + // 2. Purge des signalements obsolètes + const deletedReports = await this.databaseService.db + .delete(reports) + .where(lte(reports.expiresAt, now)) + .returning(); + this.logger.log(`Purged ${deletedReports.length} obsolete reports.`); + + // 3. Purge des utilisateurs supprimés (Soft Delete > 30 jours) + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + const deletedUsers = await this.databaseService.db + .delete(users) + .where( + and(eq(users.status, "deleted"), lte(users.deletedAt, thirtyDaysAgo)), + ) + .returning(); + this.logger.log( + `Purged ${deletedUsers.length} users marked for deletion more than 30 days ago.`, + ); + + // 4. Purge des contenus supprimés (Soft Delete > 30 jours) + const deletedContents = await this.databaseService.db + .delete(contents) + .where( + and(isNotNull(contents.deletedAt), lte(contents.deletedAt, thirtyDaysAgo)), + ) + .returning(); + this.logger.log( + `Purged ${deletedContents.length} contents marked for deletion more than 30 days ago.`, + ); + } catch (error) { + this.logger.error("Error during data purge", error); + } + } +}