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
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user