feat: implement UsersModule with service, controller, and DTOs

Added UsersModule to manage user-related operations. Includes UsersService for CRUD operations, consent updates, and 2FA handling. Implemented UsersController with endpoints for public profiles, account management, and admin user listing. Integrated with CryptoService and database schemas.
This commit is contained in:
Mathis HERRIOT
2026-01-08 15:27:20 +01:00
parent da5f18bf92
commit add7cab7df
5 changed files with 364 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
import { Injectable } from "@nestjs/common";
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 { UpdateUserDto } from "./dto/update-user.dto";
@Injectable()
export class UsersService {
constructor(
private readonly databaseService: DatabaseService,
private readonly cryptoService: CryptoService,
) {}
async create(data: {
username: string;
email: string;
passwordHash: string;
emailHash: string;
}) {
const pgpKey = this.cryptoService.getPgpEncryptionKey();
const [newUser] = await this.databaseService.db
.insert(users)
.values({
username: data.username,
email: sql`pgp_sym_encrypt(${data.email}, ${pgpKey})`,
emailHash: data.emailHash,
passwordHash: data.passwordHash,
})
.returning();
return newUser;
}
async findByEmailHash(emailHash: string) {
const pgpKey = this.cryptoService.getPgpEncryptionKey();
const result = await this.databaseService.db
.select({
uuid: users.uuid,
username: users.username,
email: sql<string>`pgp_sym_decrypt(${users.email}, ${pgpKey})`,
passwordHash: users.passwordHash,
status: users.status,
isTwoFactorEnabled: users.isTwoFactorEnabled,
})
.from(users)
.where(eq(users.emailHash, emailHash))
.limit(1);
return result[0] || null;
}
async findOneWithPrivateData(uuid: string) {
const pgpKey = this.cryptoService.getPgpEncryptionKey();
const result = await this.databaseService.db
.select({
uuid: users.uuid,
username: users.username,
email: sql<string>`pgp_sym_decrypt(${users.email}, ${pgpKey})`,
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;
}
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);
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;
}
async findOne(uuid: string) {
const result = await this.databaseService.db
.select()
.from(users)
.where(eq(users.uuid, uuid))
.limit(1);
return result[0] || null;
}
async update(uuid: string, data: UpdateUserDto) {
return await this.databaseService.db
.update(users)
.set({ ...data, updatedAt: new Date() })
.where(eq(users.uuid, uuid))
.returning();
}
async updateConsent(
uuid: string,
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();
}
async setTwoFactorSecret(uuid: string, secret: string) {
const pgpKey = this.cryptoService.getPgpEncryptionKey();
return await this.databaseService.db
.update(users)
.set({
twoFactorSecret: sql`pgp_sym_encrypt(${secret}, ${pgpKey})`,
updatedAt: new Date(),
})
.where(eq(users.uuid, uuid))
.returning();
}
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();
}
async getTwoFactorSecret(uuid: string): Promise<string | null> {
const pgpKey = this.cryptoService.getPgpEncryptionKey();
const result = await this.databaseService.db
.select({
secret: sql<string>`pgp_sym_decrypt(${users.twoFactorSecret}, ${pgpKey})`,
})
.from(users)
.where(eq(users.uuid, uuid))
.limit(1);
return result[0]?.secret || null;
}
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));
return {
profile: user,
contents: userContents,
favorites: userFavorites,
exportedAt: new Date(),
};
}
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;
});
}
}