feat: add modular services and repositories for improved code organization

Introduce repository pattern across multiple services, including `favorites`, `tags`, `sessions`, `reports`, `auth`, and more. Decouple crypto functionalities into modular services like `HashingService`, `JwtService`, and `EncryptionService`. Improve testability and maintainability by simplifying dependencies and consolidating utility logic.
This commit is contained in:
Mathis HERRIOT
2026-01-14 12:11:39 +01:00
parent 9c45bf11e4
commit 514bd354bf
64 changed files with 1801 additions and 1295 deletions

View File

@@ -1,14 +1,7 @@
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";
import {
contents,
favorites,
users,
} from "../database/schemas";
import { UsersRepository } from "./repositories/users.repository";
import { UpdateUserDto } from "./dto/update-user.dto";
@Injectable()
@@ -16,8 +9,7 @@ export class UsersService {
private readonly logger = new Logger(UsersService.name);
constructor(
private readonly databaseService: DatabaseService,
private readonly cryptoService: CryptoService,
private readonly usersRepository: UsersRepository,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
@@ -33,109 +25,37 @@ export class UsersService {
passwordHash: string;
emailHash: string;
}) {
const [newUser] = await this.databaseService.db
.insert(users)
.values({
username: data.username,
email: data.email,
emailHash: data.emailHash,
passwordHash: data.passwordHash,
})
.returning();
return newUser;
return await this.usersRepository.create(data);
}
async findByEmailHash(emailHash: string) {
const result = await this.databaseService.db
.select({
uuid: users.uuid,
username: users.username,
email: users.email,
passwordHash: users.passwordHash,
status: users.status,
isTwoFactorEnabled: users.isTwoFactorEnabled,
})
.from(users)
.where(eq(users.emailHash, emailHash))
.limit(1);
return result[0] || null;
return await this.usersRepository.findByEmailHash(emailHash);
}
async findOneWithPrivateData(uuid: string) {
const result = await this.databaseService.db
.select({
uuid: users.uuid,
username: users.username,
email: users.email,
displayName: users.displayName,
status: users.status,
isTwoFactorEnabled: users.isTwoFactorEnabled,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.where(eq(users.uuid, uuid))
.limit(1);
return result[0] || null;
return await this.usersRepository.findOneWithPrivateData(uuid);
}
async findAll(limit: number, offset: number) {
const totalCountResult = await this.databaseService.db
.select({ count: sql<number>`count(*)` })
.from(users);
const totalCount = Number(totalCountResult[0].count);
const data = await this.databaseService.db
.select({
uuid: users.uuid,
username: users.username,
displayName: users.displayName,
status: users.status,
createdAt: users.createdAt,
})
.from(users)
.limit(limit)
.offset(offset);
const [data, totalCount] = await Promise.all([
this.usersRepository.findAll(limit, offset),
this.usersRepository.countAll(),
]);
return { data, totalCount };
}
async findPublicProfile(username: string) {
const result = await this.databaseService.db
.select({
uuid: users.uuid,
username: users.username,
displayName: users.displayName,
createdAt: users.createdAt,
})
.from(users)
.where(eq(users.username, username))
.limit(1);
return result[0] || null;
return await this.usersRepository.findByUsername(username);
}
async findOne(uuid: string) {
const result = await this.databaseService.db
.select()
.from(users)
.where(eq(users.uuid, uuid))
.limit(1);
return result[0] || null;
return await this.usersRepository.findOne(uuid);
}
async update(uuid: string, data: UpdateUserDto) {
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();
const result = await this.usersRepository.update(uuid, data);
if (result[0]) {
await this.clearUserCache(result[0].username);
@@ -148,65 +68,37 @@ export class UsersService {
termsVersion: string,
privacyVersion: string,
) {
return await this.databaseService.db
.update(users)
.set({
termsVersion,
privacyVersion,
gdprAcceptedAt: new Date(),
updatedAt: new Date(),
})
.where(eq(users.uuid, uuid))
.returning();
return await this.usersRepository.update(uuid, {
termsVersion,
privacyVersion,
gdprAcceptedAt: new Date(),
});
}
async setTwoFactorSecret(uuid: string, secret: string) {
return await this.databaseService.db
.update(users)
.set({
twoFactorSecret: secret,
updatedAt: new Date(),
})
.where(eq(users.uuid, uuid))
.returning();
return await this.usersRepository.update(uuid, {
twoFactorSecret: secret,
});
}
async toggleTwoFactor(uuid: string, enabled: boolean) {
return await this.databaseService.db
.update(users)
.set({
isTwoFactorEnabled: enabled,
updatedAt: new Date(),
})
.where(eq(users.uuid, uuid))
.returning();
return await this.usersRepository.update(uuid, {
isTwoFactorEnabled: enabled,
});
}
async getTwoFactorSecret(uuid: string): Promise<string | null> {
const result = await this.databaseService.db
.select({
secret: users.twoFactorSecret,
})
.from(users)
.where(eq(users.uuid, uuid))
.limit(1);
return result[0]?.secret || null;
return await this.usersRepository.getTwoFactorSecret(uuid);
}
async exportUserData(uuid: string) {
const user = await this.findOneWithPrivateData(uuid);
if (!user) return null;
const userContents = await this.databaseService.db
.select()
.from(contents)
.where(eq(contents.userId, uuid));
const userFavorites = await this.databaseService.db
.select()
.from(favorites)
.where(eq(favorites.userId, uuid));
const [userContents, userFavorites] = await Promise.all([
this.usersRepository.getUserContents(uuid),
this.usersRepository.getUserFavorites(uuid),
]);
return {
profile: user,
@@ -217,21 +109,6 @@ export class UsersService {
}
async remove(uuid: string) {
return await this.databaseService.db.transaction(async (tx) => {
// Soft delete de l'utilisateur
const userResult = await tx
.update(users)
.set({ status: "deleted", deletedAt: new Date() })
.where(eq(users.uuid, uuid))
.returning();
// Soft delete de tous ses contenus
await tx
.update(contents)
.set({ deletedAt: new Date() })
.where(eq(contents.userId, uuid));
return userResult;
});
return await this.usersRepository.softDeleteUserAndContents(uuid);
}
}