feat: Phase 1 – Plattform-Grundgerüst (Docker, NestJS, React, Auth)
This commit is contained in:
133
apps/platform-backend/src/users/user.repository.ts
Normal file
133
apps/platform-backend/src/users/user.repository.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
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<UserRecord | null> {
|
||||
const result = await this.database.query<UserRow>(
|
||||
`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<UserRecord | null> {
|
||||
const result = await this.database.query<UserRow>(
|
||||
`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<void> {
|
||||
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<void> {
|
||||
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<AuthUser> {
|
||||
const passwordHash = await this.passwordHasher.hash(input.password);
|
||||
const result = await this.database.query<UserRow>(
|
||||
`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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user