feat: globales HTTP-Access-Log und Exception-Logging
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:
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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user