feat: Implement centralized audit logging admin interface (refs #10)
Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 4m42s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 37s
Deploy to Staging / Verify Staging (pull_request) Failing after 6s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 6s

- Add audit_logs table with categories, severities, and indexes
- Create AuditLogService and AuditLogRepository
- Add REST API endpoints for viewing and exporting logs
- Wire audit logging into auth, vehicles, admin, and backup features
- Add desktop AdminLogsPage with filters and CSV export
- Add mobile AdminLogsMobileScreen with card layout
- Implement 90-day retention cleanup job
- Remove old AuditLogPanel from AdminCatalogPage

Security fixes:
- Escape LIKE special characters to prevent pattern injection
- Limit CSV export to 5000 records to prevent memory exhaustion
- Add truncation warning headers for large exports

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-11 11:09:09 -06:00
parent 8c7de98a9a
commit c98211f4a2
30 changed files with 2897 additions and 11 deletions

View File

@@ -0,0 +1,308 @@
/**
* @ai-summary Integration tests for audit log wiring across features
* @ai-context Verifies audit logging is properly integrated into auth, vehicle, admin, and backup features
*/
import { Pool } from 'pg';
import { appConfig } from '../../../core/config/config-loader';
import { AuditLogService } from '../domain/audit-log.service';
import { AuditLogRepository } from '../data/audit-log.repository';
describe('AuditLog Feature Integration', () => {
let pool: Pool;
let repository: AuditLogRepository;
let service: AuditLogService;
const createdIds: string[] = [];
beforeAll(async () => {
pool = new Pool({
connectionString: appConfig.getDatabaseUrl(),
});
repository = new AuditLogRepository(pool);
service = new AuditLogService(repository);
});
afterAll(async () => {
// Cleanup test data
if (createdIds.length > 0) {
await pool.query(`DELETE FROM audit_logs WHERE id = ANY($1::uuid[])`, [createdIds]);
}
await pool.end();
});
describe('Vehicle logging integration', () => {
it('should create audit log with vehicle category and correct resource', async () => {
const userId = 'test-user-vehicle-123';
const vehicleId = 'vehicle-uuid-123';
const entry = await service.info(
'vehicle',
userId,
'Vehicle created: 2024 Toyota Camry',
'vehicle',
vehicleId,
{ vin: '1HGBH41JXMN109186', make: 'Toyota', model: 'Camry', year: 2024 }
);
createdIds.push(entry.id);
expect(entry.category).toBe('vehicle');
expect(entry.severity).toBe('info');
expect(entry.userId).toBe(userId);
expect(entry.action).toContain('Vehicle created');
expect(entry.resourceType).toBe('vehicle');
expect(entry.resourceId).toBe(vehicleId);
expect(entry.details).toHaveProperty('vin');
expect(entry.details).toHaveProperty('make', 'Toyota');
});
it('should log vehicle update with correct fields', async () => {
const userId = 'test-user-vehicle-456';
const vehicleId = 'vehicle-uuid-456';
const entry = await service.info(
'vehicle',
userId,
'Vehicle updated: 2024 Toyota Camry',
'vehicle',
vehicleId,
{ updatedFields: ['color', 'licensePlate'] }
);
createdIds.push(entry.id);
expect(entry.category).toBe('vehicle');
expect(entry.action).toContain('Vehicle updated');
expect(entry.details).toHaveProperty('updatedFields');
});
it('should log vehicle deletion with vehicle info', async () => {
const userId = 'test-user-vehicle-789';
const vehicleId = 'vehicle-uuid-789';
const entry = await service.info(
'vehicle',
userId,
'Vehicle deleted: 2024 Toyota Camry',
'vehicle',
vehicleId,
{ vin: '1HGBH41JXMN109186', make: 'Toyota', model: 'Camry', year: 2024 }
);
createdIds.push(entry.id);
expect(entry.category).toBe('vehicle');
expect(entry.action).toContain('Vehicle deleted');
expect(entry.resourceId).toBe(vehicleId);
});
});
describe('Auth logging integration', () => {
it('should create audit log with auth category for signup', async () => {
const userId = 'test-user-auth-123';
const entry = await service.info(
'auth',
userId,
'User signup: test@example.com',
'user',
userId,
{ email: 'test@example.com', ipAddress: '192.168.1.1' }
);
createdIds.push(entry.id);
expect(entry.category).toBe('auth');
expect(entry.severity).toBe('info');
expect(entry.userId).toBe(userId);
expect(entry.action).toContain('signup');
expect(entry.resourceType).toBe('user');
});
it('should create audit log for password reset request', async () => {
const userId = 'test-user-auth-456';
const entry = await service.info(
'auth',
userId,
'Password reset requested',
'user',
userId
);
createdIds.push(entry.id);
expect(entry.category).toBe('auth');
expect(entry.action).toBe('Password reset requested');
});
});
describe('Admin logging integration', () => {
it('should create audit log for admin user creation', async () => {
const adminId = 'admin-user-123';
const targetAdminSub = 'auth0|target-admin-456';
const entry = await service.info(
'admin',
adminId,
'Admin user created: newadmin@example.com',
'admin_user',
targetAdminSub,
{ email: 'newadmin@example.com', role: 'admin' }
);
createdIds.push(entry.id);
expect(entry.category).toBe('admin');
expect(entry.severity).toBe('info');
expect(entry.userId).toBe(adminId);
expect(entry.action).toContain('Admin user created');
expect(entry.resourceType).toBe('admin_user');
expect(entry.details).toHaveProperty('role', 'admin');
});
it('should create audit log for admin revocation', async () => {
const adminId = 'admin-user-123';
const targetAdminSub = 'auth0|target-admin-789';
const entry = await service.info(
'admin',
adminId,
'Admin user revoked: revoked@example.com',
'admin_user',
targetAdminSub,
{ email: 'revoked@example.com' }
);
createdIds.push(entry.id);
expect(entry.category).toBe('admin');
expect(entry.action).toContain('Admin user revoked');
});
it('should create audit log for admin reinstatement', async () => {
const adminId = 'admin-user-123';
const targetAdminSub = 'auth0|target-admin-reinstated';
const entry = await service.info(
'admin',
adminId,
'Admin user reinstated: reinstated@example.com',
'admin_user',
targetAdminSub,
{ email: 'reinstated@example.com' }
);
createdIds.push(entry.id);
expect(entry.category).toBe('admin');
expect(entry.action).toContain('Admin user reinstated');
});
});
describe('Backup/System logging integration', () => {
it('should create audit log for backup creation', async () => {
const adminId = 'admin-user-backup-123';
const backupId = 'backup-uuid-123';
const entry = await service.info(
'system',
adminId,
'Backup created: Manual backup',
'backup',
backupId,
{ name: 'Manual backup', includeDocuments: true }
);
createdIds.push(entry.id);
expect(entry.category).toBe('system');
expect(entry.severity).toBe('info');
expect(entry.action).toContain('Backup created');
expect(entry.resourceType).toBe('backup');
expect(entry.resourceId).toBe(backupId);
});
it('should create audit log for backup restore', async () => {
const adminId = 'admin-user-backup-456';
const backupId = 'backup-uuid-456';
const entry = await service.info(
'system',
adminId,
'Backup restored: backup-uuid-456',
'backup',
backupId,
{ safetyBackupId: 'safety-backup-uuid' }
);
createdIds.push(entry.id);
expect(entry.category).toBe('system');
expect(entry.action).toContain('Backup restored');
});
it('should create error-level audit log for backup failure', async () => {
const adminId = 'admin-user-backup-789';
const backupId = 'backup-uuid-789';
const entry = await service.error(
'system',
adminId,
'Backup failed: Daily backup',
'backup',
backupId,
{ error: 'Disk full' }
);
createdIds.push(entry.id);
expect(entry.category).toBe('system');
expect(entry.severity).toBe('error');
expect(entry.action).toContain('Backup failed');
expect(entry.details).toHaveProperty('error', 'Disk full');
});
it('should create error-level audit log for restore failure', async () => {
const adminId = 'admin-user-restore-fail';
const backupId = 'backup-uuid-restore-fail';
const entry = await service.error(
'system',
adminId,
'Backup restore failed: backup-uuid-restore-fail',
'backup',
backupId,
{ error: 'Corrupted archive', safetyBackupId: 'safety-uuid' }
);
createdIds.push(entry.id);
expect(entry.category).toBe('system');
expect(entry.severity).toBe('error');
expect(entry.action).toContain('restore failed');
});
});
describe('Cross-feature audit log queries', () => {
it('should be able to filter logs by category', async () => {
// Search for vehicle logs
const vehicleResult = await service.search(
{ category: 'vehicle' },
{ limit: 100, offset: 0 }
);
expect(vehicleResult.logs.length).toBeGreaterThan(0);
expect(vehicleResult.logs.every((log) => log.category === 'vehicle')).toBe(true);
});
it('should be able to search across all categories', async () => {
const result = await service.search(
{ search: 'created' },
{ limit: 100, offset: 0 }
);
expect(result.logs.length).toBeGreaterThan(0);
// Should find logs from vehicle and admin categories
const categories = new Set(result.logs.map((log) => log.category));
expect(categories.size).toBeGreaterThanOrEqual(1);
});
it('should be able to filter by severity across categories', async () => {
const errorResult = await service.search(
{ severity: 'error' },
{ limit: 100, offset: 0 }
);
expect(errorResult.logs.every((log) => log.severity === 'error')).toBe(true);
});
});
});