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,163 @@
/**
* @ai-summary Centralized audit logging service
* @ai-context Provides simple API for all features to log audit events
*/
import { AuditLogRepository } from '../data/audit-log.repository';
import {
AuditLogCategory,
AuditLogSeverity,
AuditLogEntry,
AuditLogFilters,
AuditLogPagination,
AuditLogSearchResult,
isValidCategory,
isValidSeverity,
} from './audit-log.types';
import { logger } from '../../../core/logging/logger';
export class AuditLogService {
constructor(private repository: AuditLogRepository) {}
/**
* Log an audit event
*
* @param category - Event category (auth, vehicle, user, system, admin)
* @param severity - Event severity (info, warning, error)
* @param userId - User who performed the action (null for system actions)
* @param action - Human-readable description of the action
* @param resourceType - Type of resource affected (optional)
* @param resourceId - ID of affected resource (optional)
* @param details - Additional structured data (optional)
*/
async log(
category: AuditLogCategory,
severity: AuditLogSeverity,
userId: string | null,
action: string,
resourceType?: string | null,
resourceId?: string | null,
details?: Record<string, unknown> | null
): Promise<AuditLogEntry> {
// Validate category
if (!isValidCategory(category)) {
const error = new Error(`Invalid audit log category: ${category}`);
logger.error('Invalid audit log category', { category });
throw error;
}
// Validate severity
if (!isValidSeverity(severity)) {
const error = new Error(`Invalid audit log severity: ${severity}`);
logger.error('Invalid audit log severity', { severity });
throw error;
}
try {
const entry = await this.repository.create({
category,
severity,
userId,
action,
resourceType,
resourceId,
details,
});
logger.debug('Audit log created', {
id: entry.id,
category,
severity,
action,
});
return entry;
} catch (error) {
logger.error('Error creating audit log', { error, category, action });
throw error;
}
}
/**
* Convenience method for info-level logs
*/
async info(
category: AuditLogCategory,
userId: string | null,
action: string,
resourceType?: string | null,
resourceId?: string | null,
details?: Record<string, unknown> | null
): Promise<AuditLogEntry> {
return this.log(category, 'info', userId, action, resourceType, resourceId, details);
}
/**
* Convenience method for warning-level logs
*/
async warning(
category: AuditLogCategory,
userId: string | null,
action: string,
resourceType?: string | null,
resourceId?: string | null,
details?: Record<string, unknown> | null
): Promise<AuditLogEntry> {
return this.log(category, 'warning', userId, action, resourceType, resourceId, details);
}
/**
* Convenience method for error-level logs
*/
async error(
category: AuditLogCategory,
userId: string | null,
action: string,
resourceType?: string | null,
resourceId?: string | null,
details?: Record<string, unknown> | null
): Promise<AuditLogEntry> {
return this.log(category, 'error', userId, action, resourceType, resourceId, details);
}
/**
* Search audit logs with filters and pagination
*/
async search(
filters: AuditLogFilters,
pagination: AuditLogPagination
): Promise<AuditLogSearchResult> {
try {
return await this.repository.search(filters, pagination);
} catch (error) {
logger.error('Error searching audit logs', { error, filters });
throw error;
}
}
/**
* Get logs for CSV export (limited to 5000 records)
*/
async getForExport(filters: AuditLogFilters): Promise<{ logs: AuditLogEntry[]; truncated: boolean }> {
try {
return await this.repository.getForExport(filters);
} catch (error) {
logger.error('Error getting audit logs for export', { error, filters });
throw error;
}
}
/**
* Run retention cleanup (delete logs older than specified days)
*/
async cleanup(olderThanDays: number = 90): Promise<number> {
try {
const deletedCount = await this.repository.cleanup(olderThanDays);
logger.info('Audit log cleanup completed', { olderThanDays, deletedCount });
return deletedCount;
} catch (error) {
logger.error('Error running audit log cleanup', { error, olderThanDays });
throw error;
}
}
}