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>
164 lines
4.5 KiB
TypeScript
164 lines
4.5 KiB
TypeScript
/**
|
|
* @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;
|
|
}
|
|
}
|
|
}
|