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
@@ -0,0 +1,80 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { requestLoggerMiddleware } from './request-logger.middleware';
|
||||
|
||||
describe('requestLoggerMiddleware', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function fakeRequest(): Request {
|
||||
return { method: 'GET', originalUrl: '/api/v1/dashboard' } as Request;
|
||||
}
|
||||
|
||||
function fakeResponse(statusCode: number): {
|
||||
res: Response;
|
||||
finish: () => void;
|
||||
} {
|
||||
let finishHandler: () => void = () => undefined;
|
||||
const res = {
|
||||
statusCode,
|
||||
on: (event: string, handler: () => void) => {
|
||||
if (event === 'finish') {
|
||||
finishHandler = handler;
|
||||
}
|
||||
},
|
||||
} as unknown as Response;
|
||||
return { res, finish: () => finishHandler() };
|
||||
}
|
||||
|
||||
it('calls next() immediately without waiting for the response', () => {
|
||||
const next = jest.fn() as NextFunction;
|
||||
const { res } = fakeResponse(200);
|
||||
|
||||
requestLoggerMiddleware(fakeRequest(), res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs at log level for a 2xx response', () => {
|
||||
const logSpy = jest
|
||||
.spyOn(Logger.prototype, 'log')
|
||||
.mockImplementation(() => undefined);
|
||||
const { res, finish } = fakeResponse(200);
|
||||
|
||||
requestLoggerMiddleware(fakeRequest(), res, jest.fn());
|
||||
finish();
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('GET /api/v1/dashboard 200'),
|
||||
);
|
||||
});
|
||||
|
||||
it('logs at warn level for a 4xx response', () => {
|
||||
const warnSpy = jest
|
||||
.spyOn(Logger.prototype, 'warn')
|
||||
.mockImplementation(() => undefined);
|
||||
const { res, finish } = fakeResponse(401);
|
||||
|
||||
requestLoggerMiddleware(fakeRequest(), res, jest.fn());
|
||||
finish();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('GET /api/v1/dashboard 401'),
|
||||
);
|
||||
});
|
||||
|
||||
it('logs at error level for a 5xx response', () => {
|
||||
const errorSpy = jest
|
||||
.spyOn(Logger.prototype, 'error')
|
||||
.mockImplementation(() => undefined);
|
||||
const { res, finish } = fakeResponse(500);
|
||||
|
||||
requestLoggerMiddleware(fakeRequest(), res, jest.fn());
|
||||
finish();
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('GET /api/v1/dashboard 500'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
|
||||
const logger = new Logger('HTTP');
|
||||
|
||||
/**
|
||||
* One line per request in stdout/kubectl logs: method, path, status, and
|
||||
* timing - never the body/headers, so passwords and tokens never end up in
|
||||
* logs. Without this, a normal (non-error) request leaves zero trace,
|
||||
* which makes "did the request even arrive?" the hardest question to
|
||||
* answer when something looks broken from outside the cluster.
|
||||
*/
|
||||
export function requestLoggerMiddleware(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): void {
|
||||
const start = Date.now();
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
const line = `${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`;
|
||||
if (res.statusCode >= 500) {
|
||||
logger.error(line);
|
||||
} else if (res.statusCode >= 400) {
|
||||
logger.warn(line);
|
||||
} else {
|
||||
logger.log(line);
|
||||
}
|
||||
});
|
||||
next();
|
||||
}
|
||||
Reference in new issue
Block a user