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:
224
backend/src/users/users.service.ts
Normal file
224
backend/src/users/users.service.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user