/** * @ai-summary Fastify route handlers for admin management API * @ai-context HTTP request/response handling with admin authorization and audit logging */ import { FastifyRequest, FastifyReply } from 'fastify'; import { AdminService } from '../domain/admin.service'; import { AdminRepository } from '../data/admin.repository'; import { UserProfileRepository } from '../../user-profile/data/user-profile.repository'; import { pool } from '../../../core/config/database'; import { logger } from '../../../core/logging/logger'; import { CreateAdminInput, AdminIdInput, AuditLogsQueryInput, BulkCreateAdminInput, BulkRevokeAdminInput, BulkReinstateAdminInput } from './admin.validation'; import { createAdminSchema, adminIdSchema, auditLogsQuerySchema, bulkCreateAdminSchema, bulkRevokeAdminSchema, bulkReinstateAdminSchema } from './admin.validation'; import { BulkCreateAdminResponse, BulkRevokeAdminResponse, BulkReinstateAdminResponse, AdminUser } from '../domain/admin.types'; export class AdminController { private adminService: AdminService; private userProfileRepository: UserProfileRepository; constructor() { const repository = new AdminRepository(pool); this.adminService = new AdminService(repository); this.userProfileRepository = new UserProfileRepository(pool); } /** * GET /api/admin/verify - Verify admin access (for frontend auth checks) */ async verifyAccess(request: FastifyRequest, reply: FastifyReply) { try { const userId = request.userContext?.userId; const userEmail = this.resolveUserEmail(request); if (userEmail && request.userContext) { request.userContext.email = userEmail.toLowerCase(); } if (!userId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } const adminRecord = await this.adminService.getAdminByUserProfileId(userId); if (adminRecord && !adminRecord.revokedAt) { if (request.userContext) { request.userContext.isAdmin = true; request.userContext.adminRecord = adminRecord; } return reply.code(200).send({ isAdmin: true, adminRecord: { id: adminRecord.id, userProfileId: adminRecord.userProfileId, email: adminRecord.email, role: adminRecord.role } }); } if (request.userContext) { request.userContext.isAdmin = false; request.userContext.adminRecord = undefined; } return reply.code(200).send({ isAdmin: false, adminRecord: null }); } catch (error) { logger.error('Error verifying admin access', { error: error instanceof Error ? error.message : 'Unknown error', userId: request.userContext?.userId?.substring(0, 8) + '...' }); return reply.code(500).send({ error: 'Internal server error', message: 'Admin verification failed' }); } } /** * GET /api/admin/admins - List all admin users */ async listAdmins(request: FastifyRequest, reply: FastifyReply) { try { const actorUserProfileId = request.userContext?.userId; if (!actorUserProfileId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } const admins = await this.adminService.getAllAdmins(); return reply.code(200).send({ total: admins.length, admins }); } catch (error: any) { logger.error('Error listing admins', { error: error.message, actorUserProfileId: request.userContext?.userId }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to list admins' }); } } /** * POST /api/admin/admins - Create new admin user */ async createAdmin( request: FastifyRequest<{ Body: CreateAdminInput }>, reply: FastifyReply ) { try { const actorUserProfileId = request.userContext?.userId; if (!actorUserProfileId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } // Get actor's admin record to get admin ID const actorAdmin = await this.adminService.getAdminByUserProfileId(actorUserProfileId); if (!actorAdmin) { return reply.code(403).send({ error: 'Forbidden', message: 'Actor is not an admin' }); } // Validate request body const validation = createAdminSchema.safeParse(request.body); if (!validation.success) { return reply.code(400).send({ error: 'Bad Request', message: 'Invalid request body', details: validation.error.errors }); } const { email, role } = validation.data; // Look up user profile by email to get UUID const userProfile = await this.userProfileRepository.getByEmail(email); if (!userProfile) { return reply.code(404).send({ error: 'Not Found', message: `No user profile found with email ${email}. User must sign up first.` }); } const admin = await this.adminService.createAdmin( email, role, userProfile.id, actorAdmin.id ); return reply.code(201).send(admin); } catch (error: any) { logger.error('Error creating admin', { error: error.message, actorUserProfileId: request.userContext?.userId }); if (error.message.includes('already exists')) { return reply.code(400).send({ error: 'Bad Request', message: error.message }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to create admin' }); } } /** * PATCH /api/admin/admins/:id/revoke - Revoke admin access */ async revokeAdmin( request: FastifyRequest<{ Params: AdminIdInput }>, reply: FastifyReply ) { try { const actorUserProfileId = request.userContext?.userId; if (!actorUserProfileId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } // Get actor's admin record const actorAdmin = await this.adminService.getAdminByUserProfileId(actorUserProfileId); if (!actorAdmin) { return reply.code(403).send({ error: 'Forbidden', message: 'Actor is not an admin' }); } // Validate params const validation = adminIdSchema.safeParse(request.params); if (!validation.success) { return reply.code(400).send({ error: 'Bad Request', message: 'Invalid admin ID parameter', details: validation.error.errors }); } const { id } = validation.data; // Check if admin exists const targetAdmin = await this.adminService.getAdminById(id); if (!targetAdmin) { return reply.code(404).send({ error: 'Not Found', message: 'Admin user not found' }); } // Revoke the admin (service handles last admin check) const admin = await this.adminService.revokeAdmin(id, actorAdmin.id); return reply.code(200).send(admin); } catch (error: any) { logger.error('Error revoking admin', { error: error.message, actorUserProfileId: request.userContext?.userId, targetAdminId: (request.params as any).id }); if (error.message.includes('Cannot revoke the last active admin')) { return reply.code(400).send({ error: 'Bad Request', message: error.message }); } if (error.message.includes('not found')) { return reply.code(404).send({ error: 'Not Found', message: error.message }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to revoke admin' }); } } /** * PATCH /api/admin/admins/:id/reinstate - Restore revoked admin */ async reinstateAdmin( request: FastifyRequest<{ Params: AdminIdInput }>, reply: FastifyReply ) { try { const actorUserProfileId = request.userContext?.userId; if (!actorUserProfileId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } // Get actor's admin record const actorAdmin = await this.adminService.getAdminByUserProfileId(actorUserProfileId); if (!actorAdmin) { return reply.code(403).send({ error: 'Forbidden', message: 'Actor is not an admin' }); } // Validate params const validation = adminIdSchema.safeParse(request.params); if (!validation.success) { return reply.code(400).send({ error: 'Bad Request', message: 'Invalid admin ID parameter', details: validation.error.errors }); } const { id } = validation.data; // Check if admin exists const targetAdmin = await this.adminService.getAdminById(id); if (!targetAdmin) { return reply.code(404).send({ error: 'Not Found', message: 'Admin user not found' }); } // Reinstate the admin const admin = await this.adminService.reinstateAdmin(id, actorAdmin.id); return reply.code(200).send(admin); } catch (error: any) { logger.error('Error reinstating admin', { error: error.message, actorUserProfileId: request.userContext?.userId, targetAdminId: (request.params as any).id }); if (error.message.includes('not found')) { return reply.code(404).send({ error: 'Not Found', message: error.message }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to reinstate admin' }); } } /** * GET /api/admin/audit-logs - Fetch audit trail */ async getAuditLogs( request: FastifyRequest<{ Querystring: AuditLogsQueryInput }>, reply: FastifyReply ) { try { const actorId = request.userContext?.userId; if (!actorId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } // Validate query params const validation = auditLogsQuerySchema.safeParse(request.query); if (!validation.success) { return reply.code(400).send({ error: 'Bad Request', message: 'Invalid query parameters', details: validation.error.errors }); } const { limit, offset } = validation.data; const result = await this.adminService.getAuditLogs(limit, offset); return reply.code(200).send(result); } catch (error: any) { logger.error('Error fetching audit logs', { error: error.message, actorId: request.userContext?.userId }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to fetch audit logs' }); } } /** * POST /api/admin/admins/bulk - Create multiple admin users */ async bulkCreateAdmins( request: FastifyRequest<{ Body: BulkCreateAdminInput }>, reply: FastifyReply ) { try { const actorUserProfileId = request.userContext?.userId; if (!actorUserProfileId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } // Get actor's admin record const actorAdmin = await this.adminService.getAdminByUserProfileId(actorUserProfileId); if (!actorAdmin) { return reply.code(403).send({ error: 'Forbidden', message: 'Actor is not an admin' }); } // Validate request body const validation = bulkCreateAdminSchema.safeParse(request.body); if (!validation.success) { return reply.code(400).send({ error: 'Bad Request', message: 'Invalid request body', details: validation.error.errors }); } const { admins } = validation.data; const created: AdminUser[] = []; const failed: Array<{ email: string; error: string }> = []; // Process each admin creation sequentially to maintain data consistency for (const adminInput of admins) { try { const { email, role = 'admin' } = adminInput; // Look up user profile by email to get UUID const userProfile = await this.userProfileRepository.getByEmail(email); if (!userProfile) { failed.push({ email, error: `No user profile found with email ${email}. User must sign up first.` }); continue; } const admin = await this.adminService.createAdmin( email, role, userProfile.id, actorAdmin.id ); created.push(admin); } catch (error: any) { logger.error('Error creating admin in bulk operation', { error: error.message, email: adminInput.email, actorAdminId: actorAdmin.id }); failed.push({ email: adminInput.email, error: error.message || 'Failed to create admin' }); } } const response: BulkCreateAdminResponse = { created, failed }; // Return 207 Multi-Status if there were any failures, 201 if all succeeded const statusCode = failed.length > 0 ? 207 : 201; return reply.code(statusCode).send(response); } catch (error: any) { logger.error('Error in bulk create admins', { error: error.message, actorUserProfileId: request.userContext?.userId }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to process bulk admin creation' }); } } /** * PATCH /api/admin/admins/bulk-revoke - Revoke multiple admin users */ async bulkRevokeAdmins( request: FastifyRequest<{ Body: BulkRevokeAdminInput }>, reply: FastifyReply ) { try { const actorUserProfileId = request.userContext?.userId; if (!actorUserProfileId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } // Get actor's admin record const actorAdmin = await this.adminService.getAdminByUserProfileId(actorUserProfileId); if (!actorAdmin) { return reply.code(403).send({ error: 'Forbidden', message: 'Actor is not an admin' }); } // Validate request body const validation = bulkRevokeAdminSchema.safeParse(request.body); if (!validation.success) { return reply.code(400).send({ error: 'Bad Request', message: 'Invalid request body', details: validation.error.errors }); } const { ids } = validation.data; const revoked: AdminUser[] = []; const failed: Array<{ id: string; error: string }> = []; // Process each revocation sequentially to maintain data consistency for (const id of ids) { try { // Check if admin exists const targetAdmin = await this.adminService.getAdminById(id); if (!targetAdmin) { failed.push({ id, error: 'Admin user not found' }); continue; } // Attempt to revoke the admin const admin = await this.adminService.revokeAdmin(id, actorAdmin.id); revoked.push(admin); } catch (error: any) { logger.error('Error revoking admin in bulk operation', { error: error.message, adminId: id, actorAdminId: actorAdmin.id }); failed.push({ id, error: error.message || 'Failed to revoke admin' }); } } const response: BulkRevokeAdminResponse = { revoked, failed }; // Return 207 Multi-Status if there were any failures, 200 if all succeeded const statusCode = failed.length > 0 ? 207 : 200; return reply.code(statusCode).send(response); } catch (error: any) { logger.error('Error in bulk revoke admins', { error: error.message, actorUserProfileId: request.userContext?.userId }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to process bulk admin revocation' }); } } /** * PATCH /api/admin/admins/bulk-reinstate - Reinstate multiple revoked admin users */ async bulkReinstateAdmins( request: FastifyRequest<{ Body: BulkReinstateAdminInput }>, reply: FastifyReply ) { try { const actorUserProfileId = request.userContext?.userId; if (!actorUserProfileId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } // Get actor's admin record const actorAdmin = await this.adminService.getAdminByUserProfileId(actorUserProfileId); if (!actorAdmin) { return reply.code(403).send({ error: 'Forbidden', message: 'Actor is not an admin' }); } // Validate request body const validation = bulkReinstateAdminSchema.safeParse(request.body); if (!validation.success) { return reply.code(400).send({ error: 'Bad Request', message: 'Invalid request body', details: validation.error.errors }); } const { ids } = validation.data; const reinstated: AdminUser[] = []; const failed: Array<{ id: string; error: string }> = []; // Process each reinstatement sequentially to maintain data consistency for (const id of ids) { try { // Check if admin exists const targetAdmin = await this.adminService.getAdminById(id); if (!targetAdmin) { failed.push({ id, error: 'Admin user not found' }); continue; } // Attempt to reinstate the admin const admin = await this.adminService.reinstateAdmin(id, actorAdmin.id); reinstated.push(admin); } catch (error: any) { logger.error('Error reinstating admin in bulk operation', { error: error.message, adminId: id, actorAdminId: actorAdmin.id }); failed.push({ id, error: error.message || 'Failed to reinstate admin' }); } } const response: BulkReinstateAdminResponse = { reinstated, failed }; // Return 207 Multi-Status if there were any failures, 200 if all succeeded const statusCode = failed.length > 0 ? 207 : 200; return reply.code(statusCode).send(response); } catch (error: any) { logger.error('Error in bulk reinstate admins', { error: error.message, actorUserProfileId: request.userContext?.userId }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to process bulk admin reinstatement' }); } } private resolveUserEmail(request: FastifyRequest): string | undefined { const candidates: Array = [ request.userContext?.email, (request as any).user?.email, (request as any).user?.['https://motovaultpro.com/email'], (request as any).user?.['https://motovaultpro.com/user_email'], (request as any).user?.preferred_username, ]; for (const value of candidates) { if (typeof value === 'string' && value.includes('@')) { return value.trim(); } } return undefined; } }