import { EmailAddress, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { BootstrapAdminRequestDTO, ChangePasswordRequestDTO, CreateUserRequestDTO, LoginRequestDTO, ResetPasswordRequestDTO, UpdateUserRequestDTO, } from "../../common"; import { IdentityError } from "../domain/identity-error"; import type { User } from "../domain/user.aggregate"; import type { IPasswordHasher, IUserRepository } from "./identity-ports"; import type { UserInputMapper } from "./user-input.mapper"; /** Contexto interno obtenido de la sesión del servidor, nunca del body. */ export type IdentityContext = Readonly<{ userId: string; authVersion: number }>; export class IdentityUseCases { constructor( private readonly users: IUserRepository, private readonly passwords: IPasswordHasher, private readonly inputs: UserInputMapper, private readonly dummyPasswordHash: string ) {} async session(context: IdentityContext | undefined): Promise { if (!context) { return undefined; } const id = UniqueID.create(context.userId); if (!id.isSuccess) { return undefined; } const user = (await this.users.findById(id.data)).getOrUndefined(); return user?.active && user.authVersion === context.authVersion ? user : undefined; } async setupRequired() { return this.users.setupRequired(); } /** Capacidad no delegable: se consulta el usuario vigente, nunca un privilegio del cliente. */ async canManageGlobalResources(context: IdentityContext | undefined) { const actor = await this.session(context); if (!actor) { return Result.fail(new IdentityError("UNAUTHENTICATED")); } return actor.isInitialAdmin && (await this.users.isInitialAdministrator(actor.id)) ? Result.ok(undefined) : Result.fail(new IdentityError("FORBIDDEN")); } async bootstrap(dto: BootstrapAdminRequestDTO) { if (!(await this.users.setupRequired())) { return Result.fail(new IdentityError("SETUP_CLOSED")); } const user = await this.inputs.map(dto, true); return user.isSuccess ? this.users.bootstrap(user.data) : user; } async login(dto: LoginRequestDTO): Promise> { const email = EmailAddress.create(dto.email); if (!email.isSuccess) { return Result.fail(new IdentityError("INVALID_CREDENTIALS")); } const user = (await this.users.findByEmail(email.data)).getOrUndefined(); const valid = await this.passwords.verify( dto.password, user?.passwordHash.toPrimitive() ?? this.dummyPasswordHash ); if (!(valid && user?.active)) { return Result.fail(new IdentityError("INVALID_CREDENTIALS")); } return Result.ok(user); } private async administrator(context: IdentityContext) { const actor = await this.session(context); return actor?.isInitialAdmin ? Result.ok(actor) : Result.fail(new IdentityError("FORBIDDEN")); } async list(context: IdentityContext) { const access = await this.administrator(context); return access.isSuccess ? Result.ok(await this.users.list()) : access; } async create(dto: CreateUserRequestDTO, context: IdentityContext) { const access = await this.administrator(context); if (!access.isSuccess) { return access; } const user = await this.inputs.map(dto, false); return user.isSuccess ? this.users.create(user.data) : user; } async update(id: string, dto: UpdateUserRequestDTO, context: IdentityContext) { const access = await this.administrator(context); if (!access.isSuccess) { return access; } const userId = UniqueID.create(id); const profile = this.inputs.mapProfile(dto); if (!(userId.isSuccess && profile.isSuccess)) { return Result.fail(new IdentityError("INVALID_REQUEST")); } return this.users.update(userId.data, (user) => user.update({ ...profile.data, active: dto.active }) ); } async resetPassword(id: string, dto: ResetPasswordRequestDTO, context: IdentityContext) { const access = await this.administrator(context); if (!access.isSuccess) { return access; } const userId = UniqueID.create(id); if (!userId.isSuccess) { return Result.fail(new IdentityError("INVALID_REQUEST")); } if (userId.data.equals(access.data.id)) { return Result.fail(new IdentityError("ADMIN_PROTECTED")); } const hash = await this.inputs.hashPassword(dto.password); return this.users.update(userId.data, (user) => Result.ok(user.changePassword(hash))); } async changePassword(dto: ChangePasswordRequestDTO, context: IdentityContext) { const actor = await this.session(context); if (!actor) { return Result.fail(new IdentityError("UNAUTHENTICATED")); } if (!(await this.passwords.verify(dto.currentPassword, actor.passwordHash.toPrimitive()))) { return Result.fail(new IdentityError("INVALID_CREDENTIALS")); } const hash = await this.inputs.hashPassword(dto.password); return this.users.update(actor.id, (user) => { if (user.authVersion !== actor.authVersion) { return Result.fail(new IdentityError("UNAUTHENTICATED")); } return Result.ok(user.changePassword(hash)); }); } }