From fd9d1add2435f1d1984af69cefcba8014d3056f5 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:52:09 -0600 Subject: [PATCH] chore: refactor admin system for UUID identity (refs #213) Migrate admin controller, routes, validation, and users controller from auth0Sub identifiers to UUID. Admin CRUD now uses admin UUID id, user management routes use user_profiles UUID. Clean up debug logging. Co-Authored-By: Claude Opus 4.6 --- .../features/admin/api/admin.controller.ts | 261 ++++++++++-------- .../src/features/admin/api/admin.routes.ts | 44 +-- .../features/admin/api/admin.validation.ts | 14 +- .../features/admin/api/users.controller.ts | 121 ++++---- .../features/admin/api/users.validation.ts | 8 +- .../features/admin/data/admin.repository.ts | 89 +++--- .../features/admin/domain/admin.service.ts | 61 ++-- .../src/features/admin/domain/admin.types.ts | 15 +- 8 files changed, 319 insertions(+), 294 deletions(-) diff --git a/backend/src/features/admin/api/admin.controller.ts b/backend/src/features/admin/api/admin.controller.ts index 7666dee..c24e5d7 100644 --- a/backend/src/features/admin/api/admin.controller.ts +++ b/backend/src/features/admin/api/admin.controller.ts @@ -6,11 +6,12 @@ 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, - AdminAuth0SubInput, + AdminIdInput, AuditLogsQueryInput, BulkCreateAdminInput, BulkRevokeAdminInput, @@ -18,7 +19,7 @@ import { } from './admin.validation'; import { createAdminSchema, - adminAuth0SubSchema, + adminIdSchema, auditLogsQuerySchema, bulkCreateAdminSchema, bulkRevokeAdminSchema, @@ -33,10 +34,12 @@ import { 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); } /** @@ -47,49 +50,18 @@ export class AdminController { const userId = request.userContext?.userId; const userEmail = this.resolveUserEmail(request); - console.log('[DEBUG] Admin verify - userId:', userId); - console.log('[DEBUG] Admin verify - userEmail:', userEmail); - if (userEmail && request.userContext) { request.userContext.email = userEmail.toLowerCase(); } - if (!userId && !userEmail) { - console.log('[DEBUG] Admin verify - No userId or userEmail, returning 401'); + if (!userId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' }); } - let adminRecord = userId - ? await this.adminService.getAdminByAuth0Sub(userId) - : null; - - console.log('[DEBUG] Admin verify - adminRecord by auth0Sub:', adminRecord ? 'FOUND' : 'NOT FOUND'); - - // Fallback: attempt to resolve admin by email for legacy records - if (!adminRecord && userEmail) { - const emailMatch = await this.adminService.getAdminByEmail(userEmail.toLowerCase()); - - console.log('[DEBUG] Admin verify - emailMatch:', emailMatch ? 'FOUND' : 'NOT FOUND'); - if (emailMatch) { - console.log('[DEBUG] Admin verify - emailMatch.auth0Sub:', emailMatch.auth0Sub); - console.log('[DEBUG] Admin verify - emailMatch.revokedAt:', emailMatch.revokedAt); - } - - if (emailMatch && !emailMatch.revokedAt) { - // If the stored auth0Sub differs, link it to the authenticated user - if (userId && emailMatch.auth0Sub !== userId) { - console.log('[DEBUG] Admin verify - Calling linkAdminAuth0Sub to update auth0Sub'); - adminRecord = await this.adminService.linkAdminAuth0Sub(userEmail, userId); - console.log('[DEBUG] Admin verify - adminRecord after link:', adminRecord ? 'SUCCESS' : 'FAILED'); - } else { - console.log('[DEBUG] Admin verify - Using emailMatch as adminRecord'); - adminRecord = emailMatch; - } - } - } + const adminRecord = await this.adminService.getAdminByUserProfileId(userId); if (adminRecord && !adminRecord.revokedAt) { if (request.userContext) { @@ -97,12 +69,11 @@ export class AdminController { request.userContext.adminRecord = adminRecord; } - console.log('[DEBUG] Admin verify - Returning isAdmin: true'); - // User is an active admin return reply.code(200).send({ isAdmin: true, adminRecord: { - auth0Sub: adminRecord.auth0Sub, + id: adminRecord.id, + userProfileId: adminRecord.userProfileId, email: adminRecord.email, role: adminRecord.role } @@ -114,14 +85,11 @@ export class AdminController { request.userContext.adminRecord = undefined; } - console.log('[DEBUG] Admin verify - Returning isAdmin: false'); - // User is not an admin return reply.code(200).send({ isAdmin: false, adminRecord: null }); } catch (error) { - console.log('[DEBUG] Admin verify - Error caught:', error instanceof Error ? error.message : 'Unknown error'); logger.error('Error verifying admin access', { error: error instanceof Error ? error.message : 'Unknown error', userId: request.userContext?.userId?.substring(0, 8) + '...' @@ -139,9 +107,9 @@ export class AdminController { */ async listAdmins(request: FastifyRequest, reply: FastifyReply) { try { - const actorId = request.userContext?.userId; + const actorUserProfileId = request.userContext?.userId; - if (!actorId) { + if (!actorUserProfileId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing' @@ -150,11 +118,6 @@ export class AdminController { const admins = await this.adminService.getAllAdmins(); - // Log VIEW action - await this.adminService.getAdminByAuth0Sub(actorId); - // Note: Not logging VIEW as it would create excessive audit entries - // VIEW logging can be enabled if needed for compliance - return reply.code(200).send({ total: admins.length, admins @@ -162,7 +125,7 @@ export class AdminController { } catch (error: any) { logger.error('Error listing admins', { error: error.message, - actorId: request.userContext?.userId + actorUserProfileId: request.userContext?.userId }); return reply.code(500).send({ error: 'Internal server error', @@ -179,15 +142,24 @@ export class AdminController { reply: FastifyReply ) { try { - const actorId = request.userContext?.userId; + const actorUserProfileId = request.userContext?.userId; - if (!actorId) { + 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) { @@ -200,23 +172,27 @@ export class AdminController { const { email, role } = validation.data; - // Generate auth0Sub for the new admin - // In production, this should be the actual Auth0 user ID - // For now, we'll use email-based identifier - const auth0Sub = `auth0|${email.replace('@', '_at_')}`; + // 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, - auth0Sub, - actorId + userProfile.id, + actorAdmin.id ); return reply.code(201).send(admin); } catch (error: any) { logger.error('Error creating admin', { error: error.message, - actorId: request.userContext?.userId + actorUserProfileId: request.userContext?.userId }); if (error.message.includes('already exists')) { @@ -234,36 +210,45 @@ export class AdminController { } /** - * PATCH /api/admin/admins/:auth0Sub/revoke - Revoke admin access + * PATCH /api/admin/admins/:id/revoke - Revoke admin access */ async revokeAdmin( - request: FastifyRequest<{ Params: AdminAuth0SubInput }>, + request: FastifyRequest<{ Params: AdminIdInput }>, reply: FastifyReply ) { try { - const actorId = request.userContext?.userId; + const actorUserProfileId = request.userContext?.userId; - if (!actorId) { + 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 = adminAuth0SubSchema.safeParse(request.params); + const validation = adminIdSchema.safeParse(request.params); if (!validation.success) { return reply.code(400).send({ error: 'Bad Request', - message: 'Invalid auth0Sub parameter', + message: 'Invalid admin ID parameter', details: validation.error.errors }); } - const { auth0Sub } = validation.data; + const { id } = validation.data; // Check if admin exists - const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub); + const targetAdmin = await this.adminService.getAdminById(id); if (!targetAdmin) { return reply.code(404).send({ error: 'Not Found', @@ -272,14 +257,14 @@ export class AdminController { } // Revoke the admin (service handles last admin check) - const admin = await this.adminService.revokeAdmin(auth0Sub, actorId); + 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, - actorId: request.userContext?.userId, - targetAuth0Sub: request.params.auth0Sub + actorUserProfileId: request.userContext?.userId, + targetAdminId: (request.params as any).id }); if (error.message.includes('Cannot revoke the last active admin')) { @@ -304,36 +289,45 @@ export class AdminController { } /** - * PATCH /api/admin/admins/:auth0Sub/reinstate - Restore revoked admin + * PATCH /api/admin/admins/:id/reinstate - Restore revoked admin */ async reinstateAdmin( - request: FastifyRequest<{ Params: AdminAuth0SubInput }>, + request: FastifyRequest<{ Params: AdminIdInput }>, reply: FastifyReply ) { try { - const actorId = request.userContext?.userId; + const actorUserProfileId = request.userContext?.userId; - if (!actorId) { + 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 = adminAuth0SubSchema.safeParse(request.params); + const validation = adminIdSchema.safeParse(request.params); if (!validation.success) { return reply.code(400).send({ error: 'Bad Request', - message: 'Invalid auth0Sub parameter', + message: 'Invalid admin ID parameter', details: validation.error.errors }); } - const { auth0Sub } = validation.data; + const { id } = validation.data; // Check if admin exists - const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub); + const targetAdmin = await this.adminService.getAdminById(id); if (!targetAdmin) { return reply.code(404).send({ error: 'Not Found', @@ -342,14 +336,14 @@ export class AdminController { } // Reinstate the admin - const admin = await this.adminService.reinstateAdmin(auth0Sub, actorId); + 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, - actorId: request.userContext?.userId, - targetAuth0Sub: request.params.auth0Sub + actorUserProfileId: request.userContext?.userId, + targetAdminId: (request.params as any).id }); if (error.message.includes('not found')) { @@ -418,15 +412,24 @@ export class AdminController { reply: FastifyReply ) { try { - const actorId = request.userContext?.userId; + const actorUserProfileId = request.userContext?.userId; - if (!actorId) { + 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) { @@ -447,15 +450,21 @@ export class AdminController { try { const { email, role = 'admin' } = adminInput; - // Generate auth0Sub for the new admin - // In production, this should be the actual Auth0 user ID - const auth0Sub = `auth0|${email.replace('@', '_at_')}`; + // 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, - auth0Sub, - actorId + userProfile.id, + actorAdmin.id ); created.push(admin); @@ -463,7 +472,7 @@ export class AdminController { logger.error('Error creating admin in bulk operation', { error: error.message, email: adminInput.email, - actorId + actorAdminId: actorAdmin.id }); failed.push({ @@ -485,7 +494,7 @@ export class AdminController { } catch (error: any) { logger.error('Error in bulk create admins', { error: error.message, - actorId: request.userContext?.userId + actorUserProfileId: request.userContext?.userId }); return reply.code(500).send({ @@ -503,15 +512,24 @@ export class AdminController { reply: FastifyReply ) { try { - const actorId = request.userContext?.userId; + const actorUserProfileId = request.userContext?.userId; - if (!actorId) { + 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) { @@ -522,37 +540,36 @@ export class AdminController { }); } - const { auth0Subs } = validation.data; + const { ids } = validation.data; const revoked: AdminUser[] = []; - const failed: Array<{ auth0Sub: string; error: string }> = []; + const failed: Array<{ id: string; error: string }> = []; // Process each revocation sequentially to maintain data consistency - for (const auth0Sub of auth0Subs) { + for (const id of ids) { try { // Check if admin exists - const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub); + const targetAdmin = await this.adminService.getAdminById(id); if (!targetAdmin) { failed.push({ - auth0Sub, + id, error: 'Admin user not found' }); continue; } // Attempt to revoke the admin - const admin = await this.adminService.revokeAdmin(auth0Sub, actorId); + 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, - auth0Sub, - actorId + adminId: id, + actorAdminId: actorAdmin.id }); - // Special handling for "last admin" constraint failed.push({ - auth0Sub, + id, error: error.message || 'Failed to revoke admin' }); } @@ -570,7 +587,7 @@ export class AdminController { } catch (error: any) { logger.error('Error in bulk revoke admins', { error: error.message, - actorId: request.userContext?.userId + actorUserProfileId: request.userContext?.userId }); return reply.code(500).send({ @@ -588,15 +605,24 @@ export class AdminController { reply: FastifyReply ) { try { - const actorId = request.userContext?.userId; + const actorUserProfileId = request.userContext?.userId; - if (!actorId) { + 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) { @@ -607,36 +633,36 @@ export class AdminController { }); } - const { auth0Subs } = validation.data; + const { ids } = validation.data; const reinstated: AdminUser[] = []; - const failed: Array<{ auth0Sub: string; error: string }> = []; + const failed: Array<{ id: string; error: string }> = []; // Process each reinstatement sequentially to maintain data consistency - for (const auth0Sub of auth0Subs) { + for (const id of ids) { try { // Check if admin exists - const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub); + const targetAdmin = await this.adminService.getAdminById(id); if (!targetAdmin) { failed.push({ - auth0Sub, + id, error: 'Admin user not found' }); continue; } // Attempt to reinstate the admin - const admin = await this.adminService.reinstateAdmin(auth0Sub, actorId); + 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, - auth0Sub, - actorId + adminId: id, + actorAdminId: actorAdmin.id }); failed.push({ - auth0Sub, + id, error: error.message || 'Failed to reinstate admin' }); } @@ -654,7 +680,7 @@ export class AdminController { } catch (error: any) { logger.error('Error in bulk reinstate admins', { error: error.message, - actorId: request.userContext?.userId + actorUserProfileId: request.userContext?.userId }); return reply.code(500).send({ @@ -665,9 +691,6 @@ export class AdminController { } private resolveUserEmail(request: FastifyRequest): string | undefined { - console.log('[DEBUG] resolveUserEmail - request.userContext:', JSON.stringify(request.userContext, null, 2)); - console.log('[DEBUG] resolveUserEmail - request.user:', JSON.stringify((request as any).user, null, 2)); - const candidates: Array = [ request.userContext?.email, (request as any).user?.email, @@ -676,15 +699,11 @@ export class AdminController { (request as any).user?.preferred_username, ]; - console.log('[DEBUG] resolveUserEmail - candidates:', candidates); - for (const value of candidates) { if (typeof value === 'string' && value.includes('@')) { - console.log('[DEBUG] resolveUserEmail - found email:', value); return value.trim(); } } - console.log('[DEBUG] resolveUserEmail - no email found'); return undefined; } } diff --git a/backend/src/features/admin/api/admin.routes.ts b/backend/src/features/admin/api/admin.routes.ts index 857927f..d7da529 100644 --- a/backend/src/features/admin/api/admin.routes.ts +++ b/backend/src/features/admin/api/admin.routes.ts @@ -8,7 +8,7 @@ import { AdminController } from './admin.controller'; import { UsersController } from './users.controller'; import { CreateAdminInput, - AdminAuth0SubInput, + AdminIdInput, BulkCreateAdminInput, BulkRevokeAdminInput, BulkReinstateAdminInput, @@ -17,7 +17,7 @@ import { } from './admin.validation'; import { ListUsersQueryInput, - UserAuth0SubInput, + UserIdInput, UpdateTierInput, DeactivateUserInput, UpdateProfileInput, @@ -65,14 +65,14 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => { handler: adminController.createAdmin.bind(adminController) }); - // PATCH /api/admin/admins/:auth0Sub/revoke - Revoke admin access - fastify.patch<{ Params: AdminAuth0SubInput }>('/admin/admins/:auth0Sub/revoke', { + // PATCH /api/admin/admins/:id/revoke - Revoke admin access + fastify.patch<{ Params: AdminIdInput }>('/admin/admins/:id/revoke', { preHandler: [fastify.requireAdmin], handler: adminController.revokeAdmin.bind(adminController) }); - // PATCH /api/admin/admins/:auth0Sub/reinstate - Restore revoked admin - fastify.patch<{ Params: AdminAuth0SubInput }>('/admin/admins/:auth0Sub/reinstate', { + // PATCH /api/admin/admins/:id/reinstate - Restore revoked admin + fastify.patch<{ Params: AdminIdInput }>('/admin/admins/:id/reinstate', { preHandler: [fastify.requireAdmin], handler: adminController.reinstateAdmin.bind(adminController) }); @@ -117,50 +117,50 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => { handler: usersController.listUsers.bind(usersController) }); - // GET /api/admin/users/:auth0Sub - Get single user details - fastify.get<{ Params: UserAuth0SubInput }>('/admin/users/:auth0Sub', { + // GET /api/admin/users/:userId - Get single user details + fastify.get<{ Params: UserIdInput }>('/admin/users/:userId', { preHandler: [fastify.requireAdmin], handler: usersController.getUser.bind(usersController) }); - // GET /api/admin/users/:auth0Sub/vehicles - Get user's vehicles (admin view) - fastify.get<{ Params: UserAuth0SubInput }>('/admin/users/:auth0Sub/vehicles', { + // GET /api/admin/users/:userId/vehicles - Get user's vehicles (admin view) + fastify.get<{ Params: UserIdInput }>('/admin/users/:userId/vehicles', { preHandler: [fastify.requireAdmin], handler: usersController.getUserVehicles.bind(usersController) }); - // PATCH /api/admin/users/:auth0Sub/tier - Update subscription tier - fastify.patch<{ Params: UserAuth0SubInput; Body: UpdateTierInput }>('/admin/users/:auth0Sub/tier', { + // PATCH /api/admin/users/:userId/tier - Update subscription tier + fastify.patch<{ Params: UserIdInput; Body: UpdateTierInput }>('/admin/users/:userId/tier', { preHandler: [fastify.requireAdmin], handler: usersController.updateTier.bind(usersController) }); - // PATCH /api/admin/users/:auth0Sub/deactivate - Soft delete user - fastify.patch<{ Params: UserAuth0SubInput; Body: DeactivateUserInput }>('/admin/users/:auth0Sub/deactivate', { + // PATCH /api/admin/users/:userId/deactivate - Soft delete user + fastify.patch<{ Params: UserIdInput; Body: DeactivateUserInput }>('/admin/users/:userId/deactivate', { preHandler: [fastify.requireAdmin], handler: usersController.deactivateUser.bind(usersController) }); - // PATCH /api/admin/users/:auth0Sub/reactivate - Restore deactivated user - fastify.patch<{ Params: UserAuth0SubInput }>('/admin/users/:auth0Sub/reactivate', { + // PATCH /api/admin/users/:userId/reactivate - Restore deactivated user + fastify.patch<{ Params: UserIdInput }>('/admin/users/:userId/reactivate', { preHandler: [fastify.requireAdmin], handler: usersController.reactivateUser.bind(usersController) }); - // PATCH /api/admin/users/:auth0Sub/profile - Update user email/displayName - fastify.patch<{ Params: UserAuth0SubInput; Body: UpdateProfileInput }>('/admin/users/:auth0Sub/profile', { + // PATCH /api/admin/users/:userId/profile - Update user email/displayName + fastify.patch<{ Params: UserIdInput; Body: UpdateProfileInput }>('/admin/users/:userId/profile', { preHandler: [fastify.requireAdmin], handler: usersController.updateProfile.bind(usersController) }); - // PATCH /api/admin/users/:auth0Sub/promote - Promote user to admin - fastify.patch<{ Params: UserAuth0SubInput; Body: PromoteToAdminInput }>('/admin/users/:auth0Sub/promote', { + // PATCH /api/admin/users/:userId/promote - Promote user to admin + fastify.patch<{ Params: UserIdInput; Body: PromoteToAdminInput }>('/admin/users/:userId/promote', { preHandler: [fastify.requireAdmin], handler: usersController.promoteToAdmin.bind(usersController) }); - // DELETE /api/admin/users/:auth0Sub - Hard delete user (permanent) - fastify.delete<{ Params: UserAuth0SubInput }>('/admin/users/:auth0Sub', { + // DELETE /api/admin/users/:userId - Hard delete user (permanent) + fastify.delete<{ Params: UserIdInput }>('/admin/users/:userId', { preHandler: [fastify.requireAdmin], handler: usersController.hardDeleteUser.bind(usersController) }); diff --git a/backend/src/features/admin/api/admin.validation.ts b/backend/src/features/admin/api/admin.validation.ts index 2e2d9ba..2d5d45e 100644 --- a/backend/src/features/admin/api/admin.validation.ts +++ b/backend/src/features/admin/api/admin.validation.ts @@ -10,8 +10,8 @@ export const createAdminSchema = z.object({ role: z.enum(['admin', 'super_admin']).default('admin'), }); -export const adminAuth0SubSchema = z.object({ - auth0Sub: z.string().min(1, 'auth0Sub is required'), +export const adminIdSchema = z.object({ + id: z.string().uuid('Invalid admin ID format'), }); export const auditLogsQuerySchema = z.object({ @@ -29,14 +29,14 @@ export const bulkCreateAdminSchema = z.object({ }); export const bulkRevokeAdminSchema = z.object({ - auth0Subs: z.array(z.string().min(1, 'auth0Sub cannot be empty')) - .min(1, 'At least one auth0Sub must be provided') + ids: z.array(z.string().uuid('Invalid admin ID format')) + .min(1, 'At least one admin ID must be provided') .max(100, 'Maximum 100 admins per batch'), }); export const bulkReinstateAdminSchema = z.object({ - auth0Subs: z.array(z.string().min(1, 'auth0Sub cannot be empty')) - .min(1, 'At least one auth0Sub must be provided') + ids: z.array(z.string().uuid('Invalid admin ID format')) + .min(1, 'At least one admin ID must be provided') .max(100, 'Maximum 100 admins per batch'), }); @@ -49,7 +49,7 @@ export const bulkDeleteCatalogSchema = z.object({ }); export type CreateAdminInput = z.infer; -export type AdminAuth0SubInput = z.infer; +export type AdminIdInput = z.infer; export type AuditLogsQueryInput = z.infer; export type BulkCreateAdminInput = z.infer; export type BulkRevokeAdminInput = z.infer; diff --git a/backend/src/features/admin/api/users.controller.ts b/backend/src/features/admin/api/users.controller.ts index 80af1aa..70566c8 100644 --- a/backend/src/features/admin/api/users.controller.ts +++ b/backend/src/features/admin/api/users.controller.ts @@ -14,13 +14,13 @@ import { pool } from '../../../core/config/database'; import { logger } from '../../../core/logging/logger'; import { listUsersQuerySchema, - userAuth0SubSchema, + userIdSchema, updateTierSchema, deactivateUserSchema, updateProfileSchema, promoteToAdminSchema, ListUsersQueryInput, - UserAuth0SubInput, + UserIdInput, UpdateTierInput, DeactivateUserInput, UpdateProfileInput, @@ -95,10 +95,10 @@ export class UsersController { } /** - * GET /api/admin/users/:auth0Sub/vehicles - Get user's vehicles (admin view) + * GET /api/admin/users/:userId/vehicles - Get user's vehicles (admin view) */ async getUserVehicles( - request: FastifyRequest<{ Params: UserAuth0SubInput }>, + request: FastifyRequest<{ Params: UserIdInput }>, reply: FastifyReply ) { try { @@ -119,7 +119,7 @@ export class UsersController { } // Validate path param - const parseResult = userAuth0SubSchema.safeParse(request.params); + const parseResult = userIdSchema.safeParse(request.params); if (!parseResult.success) { return reply.code(400).send({ error: 'Validation error', @@ -127,14 +127,14 @@ export class UsersController { }); } - const { auth0Sub } = parseResult.data; - const vehicles = await this.userProfileRepository.getUserVehiclesForAdmin(auth0Sub); + const { userId } = parseResult.data; + const vehicles = await this.userProfileRepository.getUserVehiclesForAdmin(userId); return reply.code(200).send({ vehicles }); } catch (error) { logger.error('Error getting user vehicles', { error: error instanceof Error ? error.message : 'Unknown error', - auth0Sub: request.params?.auth0Sub, + userId: (request.params as any)?.userId, }); return reply.code(500).send({ @@ -186,10 +186,10 @@ export class UsersController { } /** - * GET /api/admin/users/:auth0Sub - Get single user details + * GET /api/admin/users/:userId - Get single user details */ async getUser( - request: FastifyRequest<{ Params: UserAuth0SubInput }>, + request: FastifyRequest<{ Params: UserIdInput }>, reply: FastifyReply ) { try { @@ -202,7 +202,7 @@ export class UsersController { } // Validate path param - const parseResult = userAuth0SubSchema.safeParse(request.params); + const parseResult = userIdSchema.safeParse(request.params); if (!parseResult.success) { return reply.code(400).send({ error: 'Validation error', @@ -210,8 +210,8 @@ export class UsersController { }); } - const { auth0Sub } = parseResult.data; - const user = await this.userProfileService.getUserDetails(auth0Sub); + const { userId } = parseResult.data; + const user = await this.userProfileService.getUserDetails(userId); if (!user) { return reply.code(404).send({ @@ -224,7 +224,7 @@ export class UsersController { } catch (error) { logger.error('Error getting user details', { error: error instanceof Error ? error.message : 'Unknown error', - auth0Sub: request.params?.auth0Sub, + userId: (request.params as any)?.userId, }); return reply.code(500).send({ @@ -235,12 +235,12 @@ export class UsersController { } /** - * PATCH /api/admin/users/:auth0Sub/tier - Update subscription tier + * PATCH /api/admin/users/:userId/tier - Update subscription tier * Uses subscriptionsService.adminOverrideTier() to sync both subscriptions.tier * and user_profiles.subscription_tier atomically */ async updateTier( - request: FastifyRequest<{ Params: UserAuth0SubInput; Body: UpdateTierInput }>, + request: FastifyRequest<{ Params: UserIdInput; Body: UpdateTierInput }>, reply: FastifyReply ) { try { @@ -253,7 +253,7 @@ export class UsersController { } // Validate path param - const paramsResult = userAuth0SubSchema.safeParse(request.params); + const paramsResult = userIdSchema.safeParse(request.params); if (!paramsResult.success) { return reply.code(400).send({ error: 'Validation error', @@ -270,11 +270,11 @@ export class UsersController { }); } - const { auth0Sub } = paramsResult.data; + const { userId } = paramsResult.data; const { subscriptionTier } = bodyResult.data; // Verify user exists before attempting tier change - const currentUser = await this.userProfileService.getUserDetails(auth0Sub); + const currentUser = await this.userProfileService.getUserDetails(userId); if (!currentUser) { return reply.code(404).send({ error: 'Not found', @@ -285,34 +285,34 @@ export class UsersController { const previousTier = currentUser.subscriptionTier; // Use subscriptionsService to update both tables atomically - await this.subscriptionsService.adminOverrideTier(auth0Sub, subscriptionTier); + await this.subscriptionsService.adminOverrideTier(userId, subscriptionTier); // Log audit action await this.adminRepository.logAuditAction( actorId, 'UPDATE_TIER', - auth0Sub, + userId, 'user_profile', currentUser.id, { previousTier, newTier: subscriptionTier } ); logger.info('User subscription tier updated via admin', { - auth0Sub, + userId, previousTier, newTier: subscriptionTier, - actorAuth0Sub: actorId, + actorId, }); // Return updated user profile - const updatedUser = await this.userProfileService.getUserDetails(auth0Sub); + const updatedUser = await this.userProfileService.getUserDetails(userId); return reply.code(200).send(updatedUser); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('Error updating user tier', { error: errorMessage, - auth0Sub: request.params?.auth0Sub, + userId: (request.params as any)?.userId, }); if (errorMessage === 'User not found') { @@ -330,10 +330,10 @@ export class UsersController { } /** - * PATCH /api/admin/users/:auth0Sub/deactivate - Soft delete user + * PATCH /api/admin/users/:userId/deactivate - Soft delete user */ async deactivateUser( - request: FastifyRequest<{ Params: UserAuth0SubInput; Body: DeactivateUserInput }>, + request: FastifyRequest<{ Params: UserIdInput; Body: DeactivateUserInput }>, reply: FastifyReply ) { try { @@ -346,7 +346,7 @@ export class UsersController { } // Validate path param - const paramsResult = userAuth0SubSchema.safeParse(request.params); + const paramsResult = userIdSchema.safeParse(request.params); if (!paramsResult.success) { return reply.code(400).send({ error: 'Validation error', @@ -363,11 +363,11 @@ export class UsersController { }); } - const { auth0Sub } = paramsResult.data; + const { userId } = paramsResult.data; const { reason } = bodyResult.data; const deactivatedUser = await this.userProfileService.deactivateUser( - auth0Sub, + userId, actorId, reason ); @@ -378,7 +378,7 @@ export class UsersController { logger.error('Error deactivating user', { error: errorMessage, - auth0Sub: request.params?.auth0Sub, + userId: (request.params as any)?.userId, }); if (errorMessage === 'User not found') { @@ -410,10 +410,10 @@ export class UsersController { } /** - * PATCH /api/admin/users/:auth0Sub/reactivate - Restore deactivated user + * PATCH /api/admin/users/:userId/reactivate - Restore deactivated user */ async reactivateUser( - request: FastifyRequest<{ Params: UserAuth0SubInput }>, + request: FastifyRequest<{ Params: UserIdInput }>, reply: FastifyReply ) { try { @@ -426,7 +426,7 @@ export class UsersController { } // Validate path param - const paramsResult = userAuth0SubSchema.safeParse(request.params); + const paramsResult = userIdSchema.safeParse(request.params); if (!paramsResult.success) { return reply.code(400).send({ error: 'Validation error', @@ -434,10 +434,10 @@ export class UsersController { }); } - const { auth0Sub } = paramsResult.data; + const { userId } = paramsResult.data; const reactivatedUser = await this.userProfileService.reactivateUser( - auth0Sub, + userId, actorId ); @@ -447,7 +447,7 @@ export class UsersController { logger.error('Error reactivating user', { error: errorMessage, - auth0Sub: request.params?.auth0Sub, + userId: (request.params as any)?.userId, }); if (errorMessage === 'User not found') { @@ -472,10 +472,10 @@ export class UsersController { } /** - * PATCH /api/admin/users/:auth0Sub/profile - Update user email/displayName + * PATCH /api/admin/users/:userId/profile - Update user email/displayName */ async updateProfile( - request: FastifyRequest<{ Params: UserAuth0SubInput; Body: UpdateProfileInput }>, + request: FastifyRequest<{ Params: UserIdInput; Body: UpdateProfileInput }>, reply: FastifyReply ) { try { @@ -488,7 +488,7 @@ export class UsersController { } // Validate path param - const paramsResult = userAuth0SubSchema.safeParse(request.params); + const paramsResult = userIdSchema.safeParse(request.params); if (!paramsResult.success) { return reply.code(400).send({ error: 'Validation error', @@ -505,11 +505,11 @@ export class UsersController { }); } - const { auth0Sub } = paramsResult.data; + const { userId } = paramsResult.data; const updates = bodyResult.data; const updatedUser = await this.userProfileService.adminUpdateProfile( - auth0Sub, + userId, updates, actorId ); @@ -520,7 +520,7 @@ export class UsersController { logger.error('Error updating user profile', { error: errorMessage, - auth0Sub: request.params?.auth0Sub, + userId: (request.params as any)?.userId, }); if (errorMessage === 'User not found') { @@ -538,10 +538,10 @@ export class UsersController { } /** - * PATCH /api/admin/users/:auth0Sub/promote - Promote user to admin + * PATCH /api/admin/users/:userId/promote - Promote user to admin */ async promoteToAdmin( - request: FastifyRequest<{ Params: UserAuth0SubInput; Body: PromoteToAdminInput }>, + request: FastifyRequest<{ Params: UserIdInput; Body: PromoteToAdminInput }>, reply: FastifyReply ) { try { @@ -554,7 +554,7 @@ export class UsersController { } // Validate path param - const paramsResult = userAuth0SubSchema.safeParse(request.params); + const paramsResult = userIdSchema.safeParse(request.params); if (!paramsResult.success) { return reply.code(400).send({ error: 'Validation error', @@ -571,11 +571,11 @@ export class UsersController { }); } - const { auth0Sub } = paramsResult.data; + const { userId } = paramsResult.data; const { role } = bodyResult.data; - // Get the user profile first to verify they exist and get their email - const user = await this.userProfileService.getUserDetails(auth0Sub); + // Get the user profile to verify they exist and get their email + const user = await this.userProfileService.getUserDetails(userId); if (!user) { return reply.code(404).send({ error: 'Not found', @@ -591,12 +591,15 @@ export class UsersController { }); } - // Create the admin record using the user's real auth0Sub + // Get actor's admin record for audit trail + const actorAdmin = await this.adminService.getAdminByUserProfileId(actorId); + + // Create the admin record using the user's UUID const adminUser = await this.adminService.createAdmin( user.email, role, - auth0Sub, // Use the real auth0Sub from the user profile - actorId + userId, + actorAdmin?.id || actorId ); return reply.code(201).send(adminUser); @@ -605,7 +608,7 @@ export class UsersController { logger.error('Error promoting user to admin', { error: errorMessage, - auth0Sub: request.params?.auth0Sub, + userId: (request.params as any)?.userId, }); if (errorMessage.includes('already exists')) { @@ -623,10 +626,10 @@ export class UsersController { } /** - * DELETE /api/admin/users/:auth0Sub - Hard delete user (permanent) + * DELETE /api/admin/users/:userId - Hard delete user (permanent) */ async hardDeleteUser( - request: FastifyRequest<{ Params: UserAuth0SubInput }>, + request: FastifyRequest<{ Params: UserIdInput }>, reply: FastifyReply ) { try { @@ -639,7 +642,7 @@ export class UsersController { } // Validate path param - const paramsResult = userAuth0SubSchema.safeParse(request.params); + const paramsResult = userIdSchema.safeParse(request.params); if (!paramsResult.success) { return reply.code(400).send({ error: 'Validation error', @@ -647,14 +650,14 @@ export class UsersController { }); } - const { auth0Sub } = paramsResult.data; + const { userId } = paramsResult.data; // Optional reason from query params const reason = (request.query as any)?.reason; // Hard delete user await this.userProfileService.adminHardDeleteUser( - auth0Sub, + userId, actorId, reason ); @@ -667,7 +670,7 @@ export class UsersController { logger.error('Error hard deleting user', { error: errorMessage, - auth0Sub: request.params?.auth0Sub, + userId: (request.params as any)?.userId, }); if (errorMessage === 'Cannot delete your own account') { diff --git a/backend/src/features/admin/api/users.validation.ts b/backend/src/features/admin/api/users.validation.ts index 5934fe2..6932691 100644 --- a/backend/src/features/admin/api/users.validation.ts +++ b/backend/src/features/admin/api/users.validation.ts @@ -19,9 +19,9 @@ export const listUsersQuerySchema = z.object({ sortOrder: z.enum(['asc', 'desc']).default('desc'), }); -// Path param for user auth0Sub -export const userAuth0SubSchema = z.object({ - auth0Sub: z.string().min(1, 'auth0Sub is required'), +// Path param for user UUID +export const userIdSchema = z.object({ + userId: z.string().uuid('Invalid user ID format'), }); // Body for updating subscription tier @@ -50,7 +50,7 @@ export const promoteToAdminSchema = z.object({ // Type exports export type ListUsersQueryInput = z.infer; -export type UserAuth0SubInput = z.infer; +export type UserIdInput = z.infer; export type UpdateTierInput = z.infer; export type DeactivateUserInput = z.infer; export type UpdateProfileInput = z.infer; diff --git a/backend/src/features/admin/data/admin.repository.ts b/backend/src/features/admin/data/admin.repository.ts index 7e40df0..7cd66c2 100644 --- a/backend/src/features/admin/data/admin.repository.ts +++ b/backend/src/features/admin/data/admin.repository.ts @@ -10,29 +10,49 @@ import { logger } from '../../../core/logging/logger'; export class AdminRepository { constructor(private pool: Pool) {} - async getAdminByAuth0Sub(auth0Sub: string): Promise { + async getAdminById(id: string): Promise { const query = ` - SELECT auth0_sub, email, role, created_at, created_by, revoked_at, updated_at + SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at FROM admin_users - WHERE auth0_sub = $1 + WHERE id = $1 LIMIT 1 `; try { - const result = await this.pool.query(query, [auth0Sub]); + const result = await this.pool.query(query, [id]); if (result.rows.length === 0) { return null; } return this.mapRowToAdminUser(result.rows[0]); } catch (error) { - logger.error('Error fetching admin by auth0_sub', { error, auth0Sub }); + logger.error('Error fetching admin by id', { error, id }); + throw error; + } + } + + async getAdminByUserProfileId(userProfileId: string): Promise { + const query = ` + SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at + FROM admin_users + WHERE user_profile_id = $1 + LIMIT 1 + `; + + try { + const result = await this.pool.query(query, [userProfileId]); + if (result.rows.length === 0) { + return null; + } + return this.mapRowToAdminUser(result.rows[0]); + } catch (error) { + logger.error('Error fetching admin by user_profile_id', { error, userProfileId }); throw error; } } async getAdminByEmail(email: string): Promise { const query = ` - SELECT auth0_sub, email, role, created_at, created_by, revoked_at, updated_at + SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at FROM admin_users WHERE LOWER(email) = LOWER($1) LIMIT 1 @@ -52,7 +72,7 @@ export class AdminRepository { async getAllAdmins(): Promise { const query = ` - SELECT auth0_sub, email, role, created_at, created_by, revoked_at, updated_at + SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at FROM admin_users ORDER BY created_at DESC `; @@ -68,7 +88,7 @@ export class AdminRepository { async getActiveAdmins(): Promise { const query = ` - SELECT auth0_sub, email, role, created_at, created_by, revoked_at, updated_at + SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at FROM admin_users WHERE revoked_at IS NULL ORDER BY created_at DESC @@ -83,61 +103,61 @@ export class AdminRepository { } } - async createAdmin(auth0Sub: string, email: string, role: string, createdBy: string): Promise { + async createAdmin(userProfileId: string, email: string, role: string, createdBy: string): Promise { const query = ` - INSERT INTO admin_users (auth0_sub, email, role, created_by) + INSERT INTO admin_users (user_profile_id, email, role, created_by) VALUES ($1, $2, $3, $4) - RETURNING auth0_sub, email, role, created_at, created_by, revoked_at, updated_at + RETURNING id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at `; try { - const result = await this.pool.query(query, [auth0Sub, email, role, createdBy]); + const result = await this.pool.query(query, [userProfileId, email, role, createdBy]); if (result.rows.length === 0) { throw new Error('Failed to create admin user'); } return this.mapRowToAdminUser(result.rows[0]); } catch (error) { - logger.error('Error creating admin', { error, auth0Sub, email }); + logger.error('Error creating admin', { error, userProfileId, email }); throw error; } } - async revokeAdmin(auth0Sub: string): Promise { + async revokeAdmin(id: string): Promise { const query = ` UPDATE admin_users SET revoked_at = CURRENT_TIMESTAMP - WHERE auth0_sub = $1 - RETURNING auth0_sub, email, role, created_at, created_by, revoked_at, updated_at + WHERE id = $1 + RETURNING id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at `; try { - const result = await this.pool.query(query, [auth0Sub]); + const result = await this.pool.query(query, [id]); if (result.rows.length === 0) { throw new Error('Admin user not found'); } return this.mapRowToAdminUser(result.rows[0]); } catch (error) { - logger.error('Error revoking admin', { error, auth0Sub }); + logger.error('Error revoking admin', { error, id }); throw error; } } - async reinstateAdmin(auth0Sub: string): Promise { + async reinstateAdmin(id: string): Promise { const query = ` UPDATE admin_users SET revoked_at = NULL - WHERE auth0_sub = $1 - RETURNING auth0_sub, email, role, created_at, created_by, revoked_at, updated_at + WHERE id = $1 + RETURNING id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at `; try { - const result = await this.pool.query(query, [auth0Sub]); + const result = await this.pool.query(query, [id]); if (result.rows.length === 0) { throw new Error('Admin user not found'); } return this.mapRowToAdminUser(result.rows[0]); } catch (error) { - logger.error('Error reinstating admin', { error, auth0Sub }); + logger.error('Error reinstating admin', { error, id }); throw error; } } @@ -202,30 +222,11 @@ export class AdminRepository { } } - async updateAuth0SubByEmail(email: string, auth0Sub: string): Promise { - const query = ` - UPDATE admin_users - SET auth0_sub = $1, - updated_at = CURRENT_TIMESTAMP - WHERE LOWER(email) = LOWER($2) - RETURNING auth0_sub, email, role, created_at, created_by, revoked_at, updated_at - `; - - try { - const result = await this.pool.query(query, [auth0Sub, email]); - if (result.rows.length === 0) { - throw new Error(`Admin user with email ${email} not found`); - } - return this.mapRowToAdminUser(result.rows[0]); - } catch (error) { - logger.error('Error updating admin auth0_sub by email', { error, email, auth0Sub }); - throw error; - } - } private mapRowToAdminUser(row: any): AdminUser { return { - auth0Sub: row.auth0_sub, + id: row.id, + userProfileId: row.user_profile_id, email: row.email, role: row.role, createdAt: new Date(row.created_at), diff --git a/backend/src/features/admin/domain/admin.service.ts b/backend/src/features/admin/domain/admin.service.ts index 00216b0..9816f41 100644 --- a/backend/src/features/admin/domain/admin.service.ts +++ b/backend/src/features/admin/domain/admin.service.ts @@ -11,11 +11,20 @@ import { auditLogService } from '../../audit-log'; export class AdminService { constructor(private repository: AdminRepository) {} - async getAdminByAuth0Sub(auth0Sub: string): Promise { + async getAdminById(id: string): Promise { try { - return await this.repository.getAdminByAuth0Sub(auth0Sub); + return await this.repository.getAdminById(id); } catch (error) { - logger.error('Error getting admin by auth0_sub', { error }); + logger.error('Error getting admin by id', { error }); + throw error; + } + } + + async getAdminByUserProfileId(userProfileId: string): Promise { + try { + return await this.repository.getAdminByUserProfileId(userProfileId); + } catch (error) { + logger.error('Error getting admin by user_profile_id', { error }); throw error; } } @@ -47,7 +56,7 @@ export class AdminService { } } - async createAdmin(email: string, role: string, auth0Sub: string, createdBy: string): Promise { + async createAdmin(email: string, role: string, userProfileId: string, createdByAdminId: string): Promise { try { // Check if admin already exists const normalizedEmail = email.trim().toLowerCase(); @@ -57,10 +66,10 @@ export class AdminService { } // Create new admin - const admin = await this.repository.createAdmin(auth0Sub, normalizedEmail, role, createdBy); + const admin = await this.repository.createAdmin(userProfileId, normalizedEmail, role, createdByAdminId); // Log audit action (legacy) - await this.repository.logAuditAction(createdBy, 'CREATE', admin.auth0Sub, 'admin_user', admin.email, { + await this.repository.logAuditAction(createdByAdminId, 'CREATE', admin.id, 'admin_user', admin.email, { email, role }); @@ -68,10 +77,10 @@ export class AdminService { // Log to unified audit log await auditLogService.info( 'admin', - createdBy, + userProfileId, `Admin user created: ${admin.email}`, 'admin_user', - admin.auth0Sub, + admin.id, { email: admin.email, role } ).catch(err => logger.error('Failed to log admin create audit event', { error: err })); @@ -83,7 +92,7 @@ export class AdminService { } } - async revokeAdmin(auth0Sub: string, revokedBy: string): Promise { + async revokeAdmin(id: string, revokedByAdminId: string): Promise { try { // Check that at least one active admin will remain const activeAdmins = await this.repository.getActiveAdmins(); @@ -92,51 +101,51 @@ export class AdminService { } // Revoke the admin - const admin = await this.repository.revokeAdmin(auth0Sub); + const admin = await this.repository.revokeAdmin(id); // Log audit action (legacy) - await this.repository.logAuditAction(revokedBy, 'REVOKE', auth0Sub, 'admin_user', admin.email); + await this.repository.logAuditAction(revokedByAdminId, 'REVOKE', id, 'admin_user', admin.email); // Log to unified audit log await auditLogService.info( 'admin', - revokedBy, + admin.userProfileId, `Admin user revoked: ${admin.email}`, 'admin_user', - auth0Sub, + id, { 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 }); + logger.info('Admin user revoked', { id, email: admin.email }); return admin; } catch (error) { - logger.error('Error revoking admin', { error, auth0Sub }); + logger.error('Error revoking admin', { error, id }); throw error; } } - async reinstateAdmin(auth0Sub: string, reinstatedBy: string): Promise { + async reinstateAdmin(id: string, reinstatedByAdminId: string): Promise { try { // Reinstate the admin - const admin = await this.repository.reinstateAdmin(auth0Sub); + const admin = await this.repository.reinstateAdmin(id); // Log audit action (legacy) - await this.repository.logAuditAction(reinstatedBy, 'REINSTATE', auth0Sub, 'admin_user', admin.email); + await this.repository.logAuditAction(reinstatedByAdminId, 'REINSTATE', id, 'admin_user', admin.email); // Log to unified audit log await auditLogService.info( 'admin', - reinstatedBy, + admin.userProfileId, `Admin user reinstated: ${admin.email}`, 'admin_user', - auth0Sub, + id, { 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 }); + logger.info('Admin user reinstated', { id, email: admin.email }); return admin; } catch (error) { - logger.error('Error reinstating admin', { error, auth0Sub }); + logger.error('Error reinstating admin', { error, id }); throw error; } } @@ -150,12 +159,4 @@ export class AdminService { } } - async linkAdminAuth0Sub(email: string, auth0Sub: string): Promise { - 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; - } - } } diff --git a/backend/src/features/admin/domain/admin.types.ts b/backend/src/features/admin/domain/admin.types.ts index ac960e1..94801c4 100644 --- a/backend/src/features/admin/domain/admin.types.ts +++ b/backend/src/features/admin/domain/admin.types.ts @@ -4,7 +4,8 @@ */ export interface AdminUser { - auth0Sub: string; + id: string; + userProfileId: string; email: string; role: 'admin' | 'super_admin'; createdAt: Date; @@ -19,11 +20,11 @@ export interface CreateAdminRequest { } export interface RevokeAdminRequest { - auth0Sub: string; + id: string; } export interface ReinstateAdminRequest { - auth0Sub: string; + id: string; } export interface AdminAuditLog { @@ -71,25 +72,25 @@ export interface BulkCreateAdminResponse { } export interface BulkRevokeAdminRequest { - auth0Subs: string[]; + ids: string[]; } export interface BulkRevokeAdminResponse { revoked: AdminUser[]; failed: Array<{ - auth0Sub: string; + id: string; error: string; }>; } export interface BulkReinstateAdminRequest { - auth0Subs: string[]; + ids: string[]; } export interface BulkReinstateAdminResponse { reinstated: AdminUser[]; failed: Array<{ - auth0Sub: string; + id: string; error: string; }>; }