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:
@@ -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() })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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() })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user