import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { PASSWORD_ALGO_ARGON2ID, hashPasswordArgon2id } from '@lisglosips/auth'; import { USERS_REPOSITORY, type UpdateUserInput, type UserSummary, type UsersRepository } from './users.repository.js'; interface CreateUserDto { username?: unknown; displayName?: unknown; phone?: unknown; email?: unknown; password?: unknown; requirePasswordChange?: unknown; roleIds?: unknown; } interface UpdateUserDto { displayName?: unknown; phone?: unknown; email?: unknown; status?: unknown; roleIds?: unknown; } interface ResetPasswordDto { password?: unknown; } @Injectable() export class UsersService { constructor(@Inject(USERS_REPOSITORY) private readonly users: UsersRepository) {} list(): Promise { return this.users.list(); } async create(body: CreateUserDto, actorId?: string): Promise { const username = this.requiredString(body.username, 'username').trim().toLowerCase(); const displayName = this.requiredString(body.displayName, 'displayName').trim(); const password = this.password(body.password); const roleIds = this.roleIds(body.roleIds); return this.users.create({ username, displayName, phone: this.optionalString(body.phone), email: this.optionalString(body.email), passwordHash: await hashPasswordArgon2id(password), passwordAlgo: PASSWORD_ALGO_ARGON2ID, requirePasswordChange: body.requirePasswordChange === undefined ? true : body.requirePasswordChange === true, roleIds, actorId }); } update(userId: string, body: UpdateUserDto, actorId?: string): Promise { const input: UpdateUserInput = { displayName: body.displayName === undefined ? undefined : this.requiredString(body.displayName, 'displayName').trim(), phone: body.phone === undefined ? undefined : this.nullableString(body.phone), email: body.email === undefined ? undefined : this.nullableString(body.email), status: body.status === undefined ? undefined : this.status(body.status), roleIds: body.roleIds === undefined ? undefined : this.roleIds(body.roleIds), actorId }; return this.users.update(userId, input); } async resetPassword(userId: string, body: ResetPasswordDto, actorId?: string): Promise { return this.users.resetPassword(userId, await hashPasswordArgon2id(this.password(body.password)), PASSWORD_ALGO_ARGON2ID, actorId); } private requiredString(value: unknown, field: string): string { if (typeof value !== 'string' || value.trim().length === 0) { throw new BadRequestException({ code: 'VALIDATION_ERROR', message: `${field} is required.` }); } return value; } private optionalString(value: unknown): string | undefined { if (value === undefined) { return undefined; } return this.requiredString(value, 'value').trim(); } private nullableString(value: unknown): string | null { if (value === null) { return null; } return this.requiredString(value, 'value').trim(); } private password(value: unknown): string { const password = this.requiredString(value, 'password'); if (password.length < 12) { throw new BadRequestException({ code: 'PASSWORD_TOO_SHORT', message: 'Password must be at least 12 characters.' }); } return password; } private roleIds(value: unknown): string[] { if (!Array.isArray(value) || value.length === 0 || !value.every((roleId) => typeof roleId === 'string' && roleId.length > 0)) { throw new BadRequestException({ code: 'ROLE_IDS_REQUIRED', message: 'At least one role is required.' }); } return [...new Set(value as string[])]; } private status(value: unknown): 'ENABLED' | 'DISABLED' { if (value !== 'ENABLED' && value !== 'DISABLED') { throw new BadRequestException({ code: 'STATUS_INVALID', message: 'Status is invalid.' }); } return value; } }