feat: Phase 1 – Plattform-Grundgerüst (Docker, NestJS, React, Auth)

This commit is contained in:
MPM Dev
2026-10-06 14:22:11 +02:00
commit a7e1c421f2
85 changed files with 17701 additions and 0 deletions

View 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,
};
}
}