feat: auditoría (audit_log en SQLite, endpoint /api/audit y pantalla /admin/auditoria)
Registra sesiones, creaciones y modificaciones en SAP (solo nombres de campos), denegados y cambios de permisos. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e9d6a757d
commit
fe8c080471
26 files changed
+993
-28
No files matched your search
@@ -3,6 +3,7 @@ import { vi } from 'vitest';
|
||||
import type { AppConfigService } from '../config/app-config.service.js';
|
||||
import type { SapClientService } from '../sap/sap-client.service.js';
|
||||
import { AuthService, escapeODataString } from './auth.service.js';
|
||||
import type { AuditService } from '../audit/audit.service.js';
|
||||
import { LoginAttemptsService } from './login-attempts.service.js';
|
||||
import { SessionStoreService } from './session-store.service.js';
|
||||
|
||||
@@ -21,12 +22,14 @@ describe('AuthService', () => {
|
||||
let get: ReturnType<typeof vi.fn>;
|
||||
let sessions: SessionStoreService;
|
||||
let service: AuthService;
|
||||
let record: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
rows = [employee()];
|
||||
get = vi.fn(async () => ({ data: { value: rows } }));
|
||||
sessions = new SessionStoreService({ get: () => 30 } as unknown as AppConfigService);
|
||||
service = new AuthService({ get } as unknown as SapClientService, sessions, new LoginAttemptsService());
|
||||
record = vi.fn(async () => undefined);
|
||||
service = new AuthService({ get } as unknown as SapClientService, sessions, new LoginAttemptsService(), { record } as unknown as AuditService);
|
||||
});
|
||||
|
||||
it('login correcto devuelve token y usuario', async () => {
|
||||
@@ -72,4 +75,45 @@ describe('AuthService', () => {
|
||||
await expect(service.login('ana@acme.com', 'secreto', '1.1.1.1')).rejects.toThrow(/Demasiados intentos/);
|
||||
expect(get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('auditoría', () => {
|
||||
it('un login correcto queda registrado con usuario e ip', async () => {
|
||||
await service.login('ana@acme.com', 'secreto', '9.9.9.9');
|
||||
expect(record).toHaveBeenCalledWith({ action: 'login', webUser: 'ana@acme.com', ip: '9.9.9.9' });
|
||||
});
|
||||
|
||||
it('un login fallido queda registrado sin distinguir usuario inexistente de clave errónea, y sin la clave', async () => {
|
||||
await service.login('ana@acme.com', 'mala-clave', '9.9.9.9').catch(() => undefined);
|
||||
rows = [];
|
||||
await service.login('nadie', 'otra-clave', '9.9.9.9').catch(() => undefined);
|
||||
const calls = record.mock.calls.map((c) => c[0]);
|
||||
expect(calls).toEqual([
|
||||
{ action: 'login_failed', webUser: 'ana@acme.com', ip: '9.9.9.9', outcome: 'error', detail: { reason: 'credenciales' } },
|
||||
{ action: 'login_failed', webUser: 'nadie', ip: '9.9.9.9', outcome: 'error', detail: { reason: 'credenciales' } },
|
||||
]);
|
||||
expect(JSON.stringify(calls)).not.toContain('clave');
|
||||
expect(JSON.stringify(calls)).not.toContain('secreto');
|
||||
});
|
||||
|
||||
it('un usuario inactivo queda registrado como denegado', async () => {
|
||||
rows = [employee({ Active: 'tNO' })];
|
||||
await service.login('ana@acme.com', 'secreto', '9.9.9.9').catch(() => undefined);
|
||||
expect(record).toHaveBeenCalledWith({ action: 'login_failed', webUser: 'ana@acme.com', ip: '9.9.9.9', outcome: 'denied', detail: { reason: 'usuario inactivo' } });
|
||||
});
|
||||
|
||||
it('el bloqueo por demasiados intentos queda registrado', async () => {
|
||||
for (let i = 0; i < 5; i++) await service.login('ana@acme.com', 'mala', '9.9.9.9').catch(() => undefined);
|
||||
record.mockClear();
|
||||
await service.login('ana@acme.com', 'secreto', '9.9.9.9').catch(() => undefined);
|
||||
expect(record).toHaveBeenCalledWith({ action: 'login_failed', webUser: 'ana@acme.com', ip: '9.9.9.9', outcome: 'denied', detail: { reason: 'demasiados intentos' } });
|
||||
});
|
||||
|
||||
it('el cierre de sesión queda registrado', async () => {
|
||||
const { token, user } = await service.login('ana@acme.com', 'secreto', '9.9.9.9');
|
||||
record.mockClear();
|
||||
await service.logout(token, user, '9.9.9.9');
|
||||
expect(sessions.validate(token)).toBeUndefined();
|
||||
expect(record).toHaveBeenCalledWith({ action: 'logout', webUser: 'ana@acme.com', ip: '9.9.9.9' });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user