feat: globales HTTP-Access-Log und Exception-Logging
Backend Image / build-and-push (push) Successful in 2m50s
Frontend Image / build-and-push (push) Successful in 2m22s

Bisher waren 4xx-Fehler (falsches Passwort, Validierungsfehler,
fehlende Berechtigung) im Log komplett unsichtbar - Nest loggt
standardmaessig nur unbehandelte 5xx-Fehler. Ein Login, das aus
Infra-Gruenden fehlschlaegt (siehe TLS/Cloudflare-Vorfall), war dadurch
allein aus den Logs nicht diagnostizierbar.

- AllExceptionsFilter (global): loggt jede Exception mit Methode/Pfad/
  Status (4xx als warn, 5xx als error mit Stacktrace). Client-Antwort
  bleibt unveraendert - HttpExceptions liefern weiter exakt ihren
  bisherigen Body, unbehandelte Fehler weiter nur eine generische
  500-Meldung ohne interne Details.
- requestLoggerMiddleware (global): eine Zeile pro Request (Methode,
  Pfad, Status, Dauer), damit auch erfolgreiche Requests im Log
  auftauchen - vorher gab's dafuer gar keine Spur.
- AuthService.login()/verifyMfaAndLogin(): konkrete Ursache pro
  Fehlschlag (unbekannter User, deaktiviert, falsches Passwort,
  falscher MFA-Code) zusaetzlich zum bestehenden Audit-Log-Eintrag.

Nirgends werden Passwoerter, Tokens oder Request-Bodies geloggt - nur
Methode, Pfad, Status, Dauer und die (bereits client-sichere)
Fehlermeldung.

Verifiziert: tsc/eslint clean, 304/304 Tests (7 neu), Build clean.
This commit is contained in:
Nick Szaunig committed 2026-08-27 07:29:51 +02:00
1 parent 05876838b0
commit ce3a0c44ac
7 files changed
+269 -5

No files matched your search

@@ -162,9 +162,12 @@ describe('AuthService', () => {
expect.objectContaining({
action: 'LOGIN_FAILED',
entityId: '[email protected]',
userId: undefined,
}),
);
const [[recordArg]] = auditLog.record.mock.calls as [
[{ userId?: string }],
];
expect(recordArg).not.toHaveProperty('userId');
});
});
+20 -4
View File
@@ -1,4 +1,4 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import * as crypto from 'node:crypto';
@@ -33,6 +33,8 @@ const REFRESH_TOKEN_BYTES = 40;
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor(
private readonly usersService: UsersService,
private readonly prisma: PrismaService,
@@ -91,17 +93,28 @@ export class AuthService {
async login(email: string, password: string): Promise<LoginResult> {
const user = await this.usersService.findByEmail(email);
if (!user || !user.isActive) {
if (!user) {
this.logger.warn(`Login failed: no user with email ${email}`);
await this.auditLog.record({
entityType: 'AuthSession',
entityId: user?.id ?? email,
entityId: email,
action: 'LOGIN_FAILED',
userId: user?.id,
});
throw new UnauthorizedException('Invalid credentials');
}
if (!user.isActive) {
this.logger.warn(`Login failed: user ${user.id} is deactivated`);
await this.auditLog.record({
entityType: 'AuthSession',
entityId: user.id,
action: 'LOGIN_FAILED',
userId: user.id,
});
throw new UnauthorizedException('Invalid credentials');
}
const passwordMatches = await bcrypt.compare(password, user.passwordHash);
if (!passwordMatches) {
this.logger.warn(`Login failed: wrong password for user ${user.id}`);
await this.auditLog.record({
entityType: 'AuthSession',
entityId: user.id,
@@ -143,6 +156,9 @@ export class AuthService {
code,
);
if (!isValid) {
this.logger.warn(
`MFA verification failed: wrong code for user ${user.id}`,
);
await this.auditLog.record({
entityType: 'AuthSession',
entityId: user.id,