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

@@ -19,6 +19,7 @@ import * as path from 'path';
import { isValidVIN, isValidPreModernVIN } from '../../../shared-minimal/utils/validators';
import { normalizeMakeName, normalizeModelName } from './name-normalizer';
import { getVehicleDataService, getPool } from '../../platform';
import { auditLogService } from '../../audit-log';
export class VehiclesService {
private readonly cachePrefix = 'vehicles';
@@ -61,9 +62,20 @@ export class VehiclesService {
// Invalidate user's vehicle list cache
await this.invalidateUserCache(userId);
// Log vehicle creation to unified audit log
const vehicleDesc = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean).join(' ');
await auditLogService.info(
'vehicle',
userId,
`Vehicle created: ${vehicleDesc || vehicle.id}`,
'vehicle',
vehicle.id,
{ vin: vehicle.vin, make: vehicle.make, model: vehicle.model, year: vehicle.year }
).catch(err => logger.error('Failed to log vehicle create audit event', { error: err }));
return this.toResponse(vehicle);
}
async getUserVehicles(userId: string): Promise<VehicleResponse[]> {
const cacheKey = `${this.cachePrefix}:user:${userId}`;
@@ -154,9 +166,20 @@ export class VehiclesService {
// Invalidate cache
await this.invalidateUserCache(userId);
// Log vehicle update to unified audit log
const vehicleDesc = [updated.year, updated.make, updated.model].filter(Boolean).join(' ');
await auditLogService.info(
'vehicle',
userId,
`Vehicle updated: ${vehicleDesc || id}`,
'vehicle',
id,
{ updatedFields: Object.keys(data) }
).catch(err => logger.error('Failed to log vehicle update audit event', { error: err }));
return this.toResponse(updated);
}
async deleteVehicle(id: string, userId: string): Promise<void> {
// Verify ownership
const existing = await this.repository.findById(id);
@@ -225,6 +248,17 @@ export class VehiclesService {
// Invalidate cache
await this.invalidateUserCache(userId);
// Log vehicle deletion to unified audit log
const vehicleDesc = [existing.year, existing.make, existing.model].filter(Boolean).join(' ');
await auditLogService.info(
'vehicle',
userId,
`Vehicle deleted: ${vehicleDesc || id}`,
'vehicle',
id,
{ vin: existing.vin, make: existing.make, model: existing.model, year: existing.year }
).catch(err => logger.error('Failed to log vehicle delete audit event', { error: err }));
}
async getVehicleRaw(id: string, userId: string): Promise<Vehicle | null> {