import { Injectable } from '@nestjs/common'; import { DatabaseService } from '../database/database.service'; import { PasswordHasher } from './password-hasher'; import type { AuthUser, RoleName, UserRecord } from './user.types'; interface UserRow { id: string; username: string; email: string; password_hash: string; display_name: string; role_name: RoleName; is_active: boolean; failed_login_attempts: number; locked_until: Date | null; last_login_at: Date | null; created_at: Date; updated_at: Date; } const USER_COLUMNS = `u.id, u.username, u.email, u.password_hash, u.display_name, r.name AS role_name, u.is_active, u.failed_login_attempts, u.locked_until, u.last_login_at, u.created_at, u.updated_at`; /** Wandelt einen Datenbank-Datensatz in die öffentliche Benutzer-Repräsentation um. */ function toAuthUser(record: UserRecord): AuthUser { return { id: record.id, username: record.username, email: record.email, displayName: record.displayName, role: record.role, }; } /** * Benutzer-Repository (Infrastructure): Alle Datenbankzugriffe für Benutzer. * Enthält keine Business-Logik – nur Datenzugriff. */ @Injectable() export class UserRepository { constructor( private readonly database: DatabaseService, private readonly passwordHasher: PasswordHasher, ) {} async findByUsername(username: string): Promise { const result = await this.database.query( `SELECT ${USER_COLUMNS} FROM users u JOIN roles r ON r.id = u.role_id WHERE u.username = $1`, [username], ); return result.rows[0] ? this.mapRow(result.rows[0]) : null; } async findById(id: string): Promise { const result = await this.database.query( `SELECT ${USER_COLUMNS} FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = $1`, [id], ); return result.rows[0] ? this.mapRow(result.rows[0]) : null; } async updateLoginSuccess(userId: string): Promise { await this.database.query( `UPDATE users SET last_login_at = now(), failed_login_attempts = 0, locked_until = NULL, updated_at = now() WHERE id = $1`, [userId], ); } async updateLoginFailure( userId: string, attempts: number, shouldLock: boolean, lockoutMinutes: number, ): Promise { await this.database.query( `UPDATE users SET failed_login_attempts = $2, locked_until = CASE WHEN $3::boolean THEN now() + make_interval(mins => $4::int) ELSE locked_until END, updated_at = now() WHERE id = $1`, [userId, attempts, shouldLock, lockoutMinutes], ); } async create(input: { username: string; email: string; password: string; displayName: string; role: RoleName; }): Promise { const passwordHash = await this.passwordHasher.hash(input.password); const result = await this.database.query( `INSERT INTO users (username, email, password_hash, display_name, role_id) VALUES ($1, $2, $3, $4, (SELECT id FROM roles WHERE name = $5)) RETURNING id, username, email, display_name, (SELECT name FROM roles WHERE id = role_id) AS role_name, true AS is_active, 0 AS failed_login_attempts, NULL::timestamptz AS locked_until, NULL::timestamptz AS last_login_at, now() AS created_at, now() AS updated_at`, [input.username, input.email, passwordHash, input.displayName, input.role], ); return toAuthUser(this.mapRow(result.rows[0])); } private mapRow(row: UserRow): UserRecord { return { id: row.id, username: row.username, email: row.email, passwordHash: row.password_hash, displayName: row.display_name, role: row.role_name, isActive: row.is_active, failedLoginAttempts: row.failed_login_attempts, lockedUntil: row.locked_until, lastLoginAt: row.last_login_at, createdAt: row.created_at, updatedAt: row.updated_at, }; } }