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:
163
backend/src/features/audit-log/domain/audit-log.service.ts
Normal file
163
backend/src/features/audit-log/domain/audit-log.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user