Files
motovaultpro/backend/src/features/admin/domain/admin.service.ts
Eric Gullickson c98211f4a2
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
feat: Implement centralized audit logging admin interface (refs #10)
- 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>
2026-01-11 11:09:09 -06:00

162 lines
5.1 KiB
TypeScript

/**
* @ai-summary Admin feature business logic
* @ai-context Handles admin user management with audit logging
*/
import { AdminRepository } from '../data/admin.repository';
import { AdminUser, AdminAuditLog } from './admin.types';
import { logger } from '../../../core/logging/logger';
import { auditLogService } from '../../audit-log';
export class AdminService {
constructor(private repository: AdminRepository) {}
async getAdminByAuth0Sub(auth0Sub: string): Promise<AdminUser | null> {
try {
return await this.repository.getAdminByAuth0Sub(auth0Sub);
} catch (error) {
logger.error('Error getting admin by auth0_sub', { error });
throw error;
}
}
async getAdminByEmail(email: string): Promise<AdminUser | null> {
try {
return await this.repository.getAdminByEmail(email);
} catch (error) {
logger.error('Error getting admin by email', { error });
throw error;
}
}
async getAllAdmins(): Promise<AdminUser[]> {
try {
return await this.repository.getAllAdmins();
} catch (error) {
logger.error('Error getting all admins', { error });
throw error;
}
}
async getActiveAdmins(): Promise<AdminUser[]> {
try {
return await this.repository.getActiveAdmins();
} catch (error) {
logger.error('Error getting active admins', { error });
throw error;
}
}
async createAdmin(email: string, role: string, auth0Sub: string, createdBy: string): Promise<AdminUser> {
try {
// Check if admin already exists
const normalizedEmail = email.trim().toLowerCase();
const existing = await this.repository.getAdminByEmail(normalizedEmail);
if (existing) {
throw new Error(`Admin user with email ${normalizedEmail} already exists`);
}
// Create new admin
const admin = await this.repository.createAdmin(auth0Sub, normalizedEmail, role, createdBy);
// Log audit action (legacy)
await this.repository.logAuditAction(createdBy, 'CREATE', admin.auth0Sub, 'admin_user', admin.email, {
email,
role
});
// Log to unified audit log
await auditLogService.info(
'admin',
createdBy,
`Admin user created: ${admin.email}`,
'admin_user',
admin.auth0Sub,
{ email: admin.email, role }
).catch(err => logger.error('Failed to log admin create audit event', { error: err }));
logger.info('Admin user created', { email, role });
return admin;
} catch (error) {
logger.error('Error creating admin', { error, email });
throw error;
}
}
async revokeAdmin(auth0Sub: string, revokedBy: string): Promise<AdminUser> {
try {
// Check that at least one active admin will remain
const activeAdmins = await this.repository.getActiveAdmins();
if (activeAdmins.length <= 1) {
throw new Error('Cannot revoke the last active admin');
}
// Revoke the admin
const admin = await this.repository.revokeAdmin(auth0Sub);
// Log audit action (legacy)
await this.repository.logAuditAction(revokedBy, 'REVOKE', auth0Sub, 'admin_user', admin.email);
// Log to unified audit log
await auditLogService.info(
'admin',
revokedBy,
`Admin user revoked: ${admin.email}`,
'admin_user',
auth0Sub,
{ email: admin.email }
).catch(err => logger.error('Failed to log admin revoke audit event', { error: err }));
logger.info('Admin user revoked', { auth0Sub, email: admin.email });
return admin;
} catch (error) {
logger.error('Error revoking admin', { error, auth0Sub });
throw error;
}
}
async reinstateAdmin(auth0Sub: string, reinstatedBy: string): Promise<AdminUser> {
try {
// Reinstate the admin
const admin = await this.repository.reinstateAdmin(auth0Sub);
// Log audit action (legacy)
await this.repository.logAuditAction(reinstatedBy, 'REINSTATE', auth0Sub, 'admin_user', admin.email);
// Log to unified audit log
await auditLogService.info(
'admin',
reinstatedBy,
`Admin user reinstated: ${admin.email}`,
'admin_user',
auth0Sub,
{ email: admin.email }
).catch(err => logger.error('Failed to log admin reinstate audit event', { error: err }));
logger.info('Admin user reinstated', { auth0Sub, email: admin.email });
return admin;
} catch (error) {
logger.error('Error reinstating admin', { error, auth0Sub });
throw error;
}
}
async getAuditLogs(limit: number = 100, offset: number = 0): Promise<{ logs: AdminAuditLog[]; total: number }> {
try {
return await this.repository.getAuditLogs(limit, offset);
} catch (error) {
logger.error('Error fetching audit logs', { error });
throw error;
}
}
async linkAdminAuth0Sub(email: string, auth0Sub: string): Promise<AdminUser> {
try {
return await this.repository.updateAuth0SubByEmail(email.trim().toLowerCase(), auth0Sub);
} catch (error) {
logger.error('Error linking admin auth0_sub to email', { error, email, auth0Sub });
throw error;
}
}
}