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>
162 lines
5.1 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|