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 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-16 09:52:09 -06:00
parent b418a503b2
commit fd9d1add24
8 changed files with 319 additions and 294 deletions

View File

@@ -6,11 +6,12 @@
import { FastifyRequest, FastifyReply } from 'fastify'; import { FastifyRequest, FastifyReply } from 'fastify';
import { AdminService } from '../domain/admin.service'; import { AdminService } from '../domain/admin.service';
import { AdminRepository } from '../data/admin.repository'; import { AdminRepository } from '../data/admin.repository';
import { UserProfileRepository } from '../../user-profile/data/user-profile.repository';
import { pool } from '../../../core/config/database'; import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger'; import { logger } from '../../../core/logging/logger';
import { import {
CreateAdminInput, CreateAdminInput,
AdminAuth0SubInput, AdminIdInput,
AuditLogsQueryInput, AuditLogsQueryInput,
BulkCreateAdminInput, BulkCreateAdminInput,
BulkRevokeAdminInput, BulkRevokeAdminInput,
@@ -18,7 +19,7 @@ import {
} from './admin.validation'; } from './admin.validation';
import { import {
createAdminSchema, createAdminSchema,
adminAuth0SubSchema, adminIdSchema,
auditLogsQuerySchema, auditLogsQuerySchema,
bulkCreateAdminSchema, bulkCreateAdminSchema,
bulkRevokeAdminSchema, bulkRevokeAdminSchema,
@@ -33,10 +34,12 @@ import {
export class AdminController { export class AdminController {
private adminService: AdminService; private adminService: AdminService;
private userProfileRepository: UserProfileRepository;
constructor() { constructor() {
const repository = new AdminRepository(pool); const repository = new AdminRepository(pool);
this.adminService = new AdminService(repository); this.adminService = new AdminService(repository);
this.userProfileRepository = new UserProfileRepository(pool);
} }
/** /**
@@ -47,49 +50,18 @@ export class AdminController {
const userId = request.userContext?.userId; const userId = request.userContext?.userId;
const userEmail = this.resolveUserEmail(request); const userEmail = this.resolveUserEmail(request);
console.log('[DEBUG] Admin verify - userId:', userId);
console.log('[DEBUG] Admin verify - userEmail:', userEmail);
if (userEmail && request.userContext) { if (userEmail && request.userContext) {
request.userContext.email = userEmail.toLowerCase(); request.userContext.email = userEmail.toLowerCase();
} }
if (!userId && !userEmail) { if (!userId) {
console.log('[DEBUG] Admin verify - No userId or userEmail, returning 401');
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing' message: 'User context missing'
}); });
} }
let adminRecord = userId const adminRecord = await this.adminService.getAdminByUserProfileId(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;
}
}
}
if (adminRecord && !adminRecord.revokedAt) { if (adminRecord && !adminRecord.revokedAt) {
if (request.userContext) { if (request.userContext) {
@@ -97,12 +69,11 @@ export class AdminController {
request.userContext.adminRecord = adminRecord; request.userContext.adminRecord = adminRecord;
} }
console.log('[DEBUG] Admin verify - Returning isAdmin: true');
// User is an active admin
return reply.code(200).send({ return reply.code(200).send({
isAdmin: true, isAdmin: true,
adminRecord: { adminRecord: {
auth0Sub: adminRecord.auth0Sub, id: adminRecord.id,
userProfileId: adminRecord.userProfileId,
email: adminRecord.email, email: adminRecord.email,
role: adminRecord.role role: adminRecord.role
} }
@@ -114,14 +85,11 @@ export class AdminController {
request.userContext.adminRecord = undefined; request.userContext.adminRecord = undefined;
} }
console.log('[DEBUG] Admin verify - Returning isAdmin: false');
// User is not an admin
return reply.code(200).send({ return reply.code(200).send({
isAdmin: false, isAdmin: false,
adminRecord: null adminRecord: null
}); });
} catch (error) { } catch (error) {
console.log('[DEBUG] Admin verify - Error caught:', error instanceof Error ? error.message : 'Unknown error');
logger.error('Error verifying admin access', { logger.error('Error verifying admin access', {
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
userId: request.userContext?.userId?.substring(0, 8) + '...' userId: request.userContext?.userId?.substring(0, 8) + '...'
@@ -139,9 +107,9 @@ export class AdminController {
*/ */
async listAdmins(request: FastifyRequest, reply: FastifyReply) { async listAdmins(request: FastifyRequest, reply: FastifyReply) {
try { try {
const actorId = request.userContext?.userId; const actorUserProfileId = request.userContext?.userId;
if (!actorId) { if (!actorUserProfileId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing' message: 'User context missing'
@@ -150,11 +118,6 @@ export class AdminController {
const admins = await this.adminService.getAllAdmins(); 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({ return reply.code(200).send({
total: admins.length, total: admins.length,
admins admins
@@ -162,7 +125,7 @@ export class AdminController {
} catch (error: any) { } catch (error: any) {
logger.error('Error listing admins', { logger.error('Error listing admins', {
error: error.message, error: error.message,
actorId: request.userContext?.userId actorUserProfileId: request.userContext?.userId
}); });
return reply.code(500).send({ return reply.code(500).send({
error: 'Internal server error', error: 'Internal server error',
@@ -179,15 +142,24 @@ export class AdminController {
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
const actorId = request.userContext?.userId; const actorUserProfileId = request.userContext?.userId;
if (!actorId) { if (!actorUserProfileId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing' 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 // Validate request body
const validation = createAdminSchema.safeParse(request.body); const validation = createAdminSchema.safeParse(request.body);
if (!validation.success) { if (!validation.success) {
@@ -200,23 +172,27 @@ export class AdminController {
const { email, role } = validation.data; const { email, role } = validation.data;
// Generate auth0Sub for the new admin // Look up user profile by email to get UUID
// In production, this should be the actual Auth0 user ID const userProfile = await this.userProfileRepository.getByEmail(email);
// For now, we'll use email-based identifier if (!userProfile) {
const auth0Sub = `auth0|${email.replace('@', '_at_')}`; 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( const admin = await this.adminService.createAdmin(
email, email,
role, role,
auth0Sub, userProfile.id,
actorId actorAdmin.id
); );
return reply.code(201).send(admin); return reply.code(201).send(admin);
} catch (error: any) { } catch (error: any) {
logger.error('Error creating admin', { logger.error('Error creating admin', {
error: error.message, error: error.message,
actorId: request.userContext?.userId actorUserProfileId: request.userContext?.userId
}); });
if (error.message.includes('already exists')) { 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( async revokeAdmin(
request: FastifyRequest<{ Params: AdminAuth0SubInput }>, request: FastifyRequest<{ Params: AdminIdInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
const actorId = request.userContext?.userId; const actorUserProfileId = request.userContext?.userId;
if (!actorId) { if (!actorUserProfileId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing' 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 // Validate params
const validation = adminAuth0SubSchema.safeParse(request.params); const validation = adminIdSchema.safeParse(request.params);
if (!validation.success) { if (!validation.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Bad Request', error: 'Bad Request',
message: 'Invalid auth0Sub parameter', message: 'Invalid admin ID parameter',
details: validation.error.errors details: validation.error.errors
}); });
} }
const { auth0Sub } = validation.data; const { id } = validation.data;
// Check if admin exists // Check if admin exists
const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub); const targetAdmin = await this.adminService.getAdminById(id);
if (!targetAdmin) { if (!targetAdmin) {
return reply.code(404).send({ return reply.code(404).send({
error: 'Not Found', error: 'Not Found',
@@ -272,14 +257,14 @@ export class AdminController {
} }
// Revoke the admin (service handles last admin check) // 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); return reply.code(200).send(admin);
} catch (error: any) { } catch (error: any) {
logger.error('Error revoking admin', { logger.error('Error revoking admin', {
error: error.message, error: error.message,
actorId: request.userContext?.userId, actorUserProfileId: request.userContext?.userId,
targetAuth0Sub: request.params.auth0Sub targetAdminId: (request.params as any).id
}); });
if (error.message.includes('Cannot revoke the last active admin')) { 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( async reinstateAdmin(
request: FastifyRequest<{ Params: AdminAuth0SubInput }>, request: FastifyRequest<{ Params: AdminIdInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
const actorId = request.userContext?.userId; const actorUserProfileId = request.userContext?.userId;
if (!actorId) { if (!actorUserProfileId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing' 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 // Validate params
const validation = adminAuth0SubSchema.safeParse(request.params); const validation = adminIdSchema.safeParse(request.params);
if (!validation.success) { if (!validation.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Bad Request', error: 'Bad Request',
message: 'Invalid auth0Sub parameter', message: 'Invalid admin ID parameter',
details: validation.error.errors details: validation.error.errors
}); });
} }
const { auth0Sub } = validation.data; const { id } = validation.data;
// Check if admin exists // Check if admin exists
const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub); const targetAdmin = await this.adminService.getAdminById(id);
if (!targetAdmin) { if (!targetAdmin) {
return reply.code(404).send({ return reply.code(404).send({
error: 'Not Found', error: 'Not Found',
@@ -342,14 +336,14 @@ export class AdminController {
} }
// Reinstate the admin // 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); return reply.code(200).send(admin);
} catch (error: any) { } catch (error: any) {
logger.error('Error reinstating admin', { logger.error('Error reinstating admin', {
error: error.message, error: error.message,
actorId: request.userContext?.userId, actorUserProfileId: request.userContext?.userId,
targetAuth0Sub: request.params.auth0Sub targetAdminId: (request.params as any).id
}); });
if (error.message.includes('not found')) { if (error.message.includes('not found')) {
@@ -418,15 +412,24 @@ export class AdminController {
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
const actorId = request.userContext?.userId; const actorUserProfileId = request.userContext?.userId;
if (!actorId) { if (!actorUserProfileId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing' 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 // Validate request body
const validation = bulkCreateAdminSchema.safeParse(request.body); const validation = bulkCreateAdminSchema.safeParse(request.body);
if (!validation.success) { if (!validation.success) {
@@ -447,15 +450,21 @@ export class AdminController {
try { try {
const { email, role = 'admin' } = adminInput; const { email, role = 'admin' } = adminInput;
// Generate auth0Sub for the new admin // Look up user profile by email to get UUID
// In production, this should be the actual Auth0 user ID const userProfile = await this.userProfileRepository.getByEmail(email);
const auth0Sub = `auth0|${email.replace('@', '_at_')}`; 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( const admin = await this.adminService.createAdmin(
email, email,
role, role,
auth0Sub, userProfile.id,
actorId actorAdmin.id
); );
created.push(admin); created.push(admin);
@@ -463,7 +472,7 @@ export class AdminController {
logger.error('Error creating admin in bulk operation', { logger.error('Error creating admin in bulk operation', {
error: error.message, error: error.message,
email: adminInput.email, email: adminInput.email,
actorId actorAdminId: actorAdmin.id
}); });
failed.push({ failed.push({
@@ -485,7 +494,7 @@ export class AdminController {
} catch (error: any) { } catch (error: any) {
logger.error('Error in bulk create admins', { logger.error('Error in bulk create admins', {
error: error.message, error: error.message,
actorId: request.userContext?.userId actorUserProfileId: request.userContext?.userId
}); });
return reply.code(500).send({ return reply.code(500).send({
@@ -503,15 +512,24 @@ export class AdminController {
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
const actorId = request.userContext?.userId; const actorUserProfileId = request.userContext?.userId;
if (!actorId) { if (!actorUserProfileId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing' 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 // Validate request body
const validation = bulkRevokeAdminSchema.safeParse(request.body); const validation = bulkRevokeAdminSchema.safeParse(request.body);
if (!validation.success) { if (!validation.success) {
@@ -522,37 +540,36 @@ export class AdminController {
}); });
} }
const { auth0Subs } = validation.data; const { ids } = validation.data;
const revoked: AdminUser[] = []; 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 // Process each revocation sequentially to maintain data consistency
for (const auth0Sub of auth0Subs) { for (const id of ids) {
try { try {
// Check if admin exists // Check if admin exists
const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub); const targetAdmin = await this.adminService.getAdminById(id);
if (!targetAdmin) { if (!targetAdmin) {
failed.push({ failed.push({
auth0Sub, id,
error: 'Admin user not found' error: 'Admin user not found'
}); });
continue; continue;
} }
// Attempt to revoke the admin // 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); revoked.push(admin);
} catch (error: any) { } catch (error: any) {
logger.error('Error revoking admin in bulk operation', { logger.error('Error revoking admin in bulk operation', {
error: error.message, error: error.message,
auth0Sub, adminId: id,
actorId actorAdminId: actorAdmin.id
}); });
// Special handling for "last admin" constraint
failed.push({ failed.push({
auth0Sub, id,
error: error.message || 'Failed to revoke admin' error: error.message || 'Failed to revoke admin'
}); });
} }
@@ -570,7 +587,7 @@ export class AdminController {
} catch (error: any) { } catch (error: any) {
logger.error('Error in bulk revoke admins', { logger.error('Error in bulk revoke admins', {
error: error.message, error: error.message,
actorId: request.userContext?.userId actorUserProfileId: request.userContext?.userId
}); });
return reply.code(500).send({ return reply.code(500).send({
@@ -588,15 +605,24 @@ export class AdminController {
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
const actorId = request.userContext?.userId; const actorUserProfileId = request.userContext?.userId;
if (!actorId) { if (!actorUserProfileId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing' 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 // Validate request body
const validation = bulkReinstateAdminSchema.safeParse(request.body); const validation = bulkReinstateAdminSchema.safeParse(request.body);
if (!validation.success) { if (!validation.success) {
@@ -607,36 +633,36 @@ export class AdminController {
}); });
} }
const { auth0Subs } = validation.data; const { ids } = validation.data;
const reinstated: AdminUser[] = []; 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 // Process each reinstatement sequentially to maintain data consistency
for (const auth0Sub of auth0Subs) { for (const id of ids) {
try { try {
// Check if admin exists // Check if admin exists
const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub); const targetAdmin = await this.adminService.getAdminById(id);
if (!targetAdmin) { if (!targetAdmin) {
failed.push({ failed.push({
auth0Sub, id,
error: 'Admin user not found' error: 'Admin user not found'
}); });
continue; continue;
} }
// Attempt to reinstate the admin // 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); reinstated.push(admin);
} catch (error: any) { } catch (error: any) {
logger.error('Error reinstating admin in bulk operation', { logger.error('Error reinstating admin in bulk operation', {
error: error.message, error: error.message,
auth0Sub, adminId: id,
actorId actorAdminId: actorAdmin.id
}); });
failed.push({ failed.push({
auth0Sub, id,
error: error.message || 'Failed to reinstate admin' error: error.message || 'Failed to reinstate admin'
}); });
} }
@@ -654,7 +680,7 @@ export class AdminController {
} catch (error: any) { } catch (error: any) {
logger.error('Error in bulk reinstate admins', { logger.error('Error in bulk reinstate admins', {
error: error.message, error: error.message,
actorId: request.userContext?.userId actorUserProfileId: request.userContext?.userId
}); });
return reply.code(500).send({ return reply.code(500).send({
@@ -665,9 +691,6 @@ export class AdminController {
} }
private resolveUserEmail(request: FastifyRequest): string | undefined { 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<string | undefined> = [ const candidates: Array<string | undefined> = [
request.userContext?.email, request.userContext?.email,
(request as any).user?.email, (request as any).user?.email,
@@ -676,15 +699,11 @@ export class AdminController {
(request as any).user?.preferred_username, (request as any).user?.preferred_username,
]; ];
console.log('[DEBUG] resolveUserEmail - candidates:', candidates);
for (const value of candidates) { for (const value of candidates) {
if (typeof value === 'string' && value.includes('@')) { if (typeof value === 'string' && value.includes('@')) {
console.log('[DEBUG] resolveUserEmail - found email:', value);
return value.trim(); return value.trim();
} }
} }
console.log('[DEBUG] resolveUserEmail - no email found');
return undefined; return undefined;
} }
} }

View File

@@ -8,7 +8,7 @@ import { AdminController } from './admin.controller';
import { UsersController } from './users.controller'; import { UsersController } from './users.controller';
import { import {
CreateAdminInput, CreateAdminInput,
AdminAuth0SubInput, AdminIdInput,
BulkCreateAdminInput, BulkCreateAdminInput,
BulkRevokeAdminInput, BulkRevokeAdminInput,
BulkReinstateAdminInput, BulkReinstateAdminInput,
@@ -17,7 +17,7 @@ import {
} from './admin.validation'; } from './admin.validation';
import { import {
ListUsersQueryInput, ListUsersQueryInput,
UserAuth0SubInput, UserIdInput,
UpdateTierInput, UpdateTierInput,
DeactivateUserInput, DeactivateUserInput,
UpdateProfileInput, UpdateProfileInput,
@@ -65,14 +65,14 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => {
handler: adminController.createAdmin.bind(adminController) handler: adminController.createAdmin.bind(adminController)
}); });
// PATCH /api/admin/admins/:auth0Sub/revoke - Revoke admin access // PATCH /api/admin/admins/:id/revoke - Revoke admin access
fastify.patch<{ Params: AdminAuth0SubInput }>('/admin/admins/:auth0Sub/revoke', { fastify.patch<{ Params: AdminIdInput }>('/admin/admins/:id/revoke', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: adminController.revokeAdmin.bind(adminController) handler: adminController.revokeAdmin.bind(adminController)
}); });
// PATCH /api/admin/admins/:auth0Sub/reinstate - Restore revoked admin // PATCH /api/admin/admins/:id/reinstate - Restore revoked admin
fastify.patch<{ Params: AdminAuth0SubInput }>('/admin/admins/:auth0Sub/reinstate', { fastify.patch<{ Params: AdminIdInput }>('/admin/admins/:id/reinstate', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: adminController.reinstateAdmin.bind(adminController) handler: adminController.reinstateAdmin.bind(adminController)
}); });
@@ -117,50 +117,50 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => {
handler: usersController.listUsers.bind(usersController) handler: usersController.listUsers.bind(usersController)
}); });
// GET /api/admin/users/:auth0Sub - Get single user details // GET /api/admin/users/:userId - Get single user details
fastify.get<{ Params: UserAuth0SubInput }>('/admin/users/:auth0Sub', { fastify.get<{ Params: UserIdInput }>('/admin/users/:userId', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: usersController.getUser.bind(usersController) handler: usersController.getUser.bind(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)
fastify.get<{ Params: UserAuth0SubInput }>('/admin/users/:auth0Sub/vehicles', { fastify.get<{ Params: UserIdInput }>('/admin/users/:userId/vehicles', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: usersController.getUserVehicles.bind(usersController) handler: usersController.getUserVehicles.bind(usersController)
}); });
// PATCH /api/admin/users/:auth0Sub/tier - Update subscription tier // PATCH /api/admin/users/:userId/tier - Update subscription tier
fastify.patch<{ Params: UserAuth0SubInput; Body: UpdateTierInput }>('/admin/users/:auth0Sub/tier', { fastify.patch<{ Params: UserIdInput; Body: UpdateTierInput }>('/admin/users/:userId/tier', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: usersController.updateTier.bind(usersController) handler: usersController.updateTier.bind(usersController)
}); });
// PATCH /api/admin/users/:auth0Sub/deactivate - Soft delete user // PATCH /api/admin/users/:userId/deactivate - Soft delete user
fastify.patch<{ Params: UserAuth0SubInput; Body: DeactivateUserInput }>('/admin/users/:auth0Sub/deactivate', { fastify.patch<{ Params: UserIdInput; Body: DeactivateUserInput }>('/admin/users/:userId/deactivate', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: usersController.deactivateUser.bind(usersController) handler: usersController.deactivateUser.bind(usersController)
}); });
// PATCH /api/admin/users/:auth0Sub/reactivate - Restore deactivated user // PATCH /api/admin/users/:userId/reactivate - Restore deactivated user
fastify.patch<{ Params: UserAuth0SubInput }>('/admin/users/:auth0Sub/reactivate', { fastify.patch<{ Params: UserIdInput }>('/admin/users/:userId/reactivate', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: usersController.reactivateUser.bind(usersController) handler: usersController.reactivateUser.bind(usersController)
}); });
// PATCH /api/admin/users/:auth0Sub/profile - Update user email/displayName // PATCH /api/admin/users/:userId/profile - Update user email/displayName
fastify.patch<{ Params: UserAuth0SubInput; Body: UpdateProfileInput }>('/admin/users/:auth0Sub/profile', { fastify.patch<{ Params: UserIdInput; Body: UpdateProfileInput }>('/admin/users/:userId/profile', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: usersController.updateProfile.bind(usersController) handler: usersController.updateProfile.bind(usersController)
}); });
// PATCH /api/admin/users/:auth0Sub/promote - Promote user to admin // PATCH /api/admin/users/:userId/promote - Promote user to admin
fastify.patch<{ Params: UserAuth0SubInput; Body: PromoteToAdminInput }>('/admin/users/:auth0Sub/promote', { fastify.patch<{ Params: UserIdInput; Body: PromoteToAdminInput }>('/admin/users/:userId/promote', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: usersController.promoteToAdmin.bind(usersController) handler: usersController.promoteToAdmin.bind(usersController)
}); });
// DELETE /api/admin/users/:auth0Sub - Hard delete user (permanent) // DELETE /api/admin/users/:userId - Hard delete user (permanent)
fastify.delete<{ Params: UserAuth0SubInput }>('/admin/users/:auth0Sub', { fastify.delete<{ Params: UserIdInput }>('/admin/users/:userId', {
preHandler: [fastify.requireAdmin], preHandler: [fastify.requireAdmin],
handler: usersController.hardDeleteUser.bind(usersController) handler: usersController.hardDeleteUser.bind(usersController)
}); });

View File

@@ -10,8 +10,8 @@ export const createAdminSchema = z.object({
role: z.enum(['admin', 'super_admin']).default('admin'), role: z.enum(['admin', 'super_admin']).default('admin'),
}); });
export const adminAuth0SubSchema = z.object({ export const adminIdSchema = z.object({
auth0Sub: z.string().min(1, 'auth0Sub is required'), id: z.string().uuid('Invalid admin ID format'),
}); });
export const auditLogsQuerySchema = z.object({ export const auditLogsQuerySchema = z.object({
@@ -29,14 +29,14 @@ export const bulkCreateAdminSchema = z.object({
}); });
export const bulkRevokeAdminSchema = z.object({ export const bulkRevokeAdminSchema = z.object({
auth0Subs: z.array(z.string().min(1, 'auth0Sub cannot be empty')) ids: z.array(z.string().uuid('Invalid admin ID format'))
.min(1, 'At least one auth0Sub must be provided') .min(1, 'At least one admin ID must be provided')
.max(100, 'Maximum 100 admins per batch'), .max(100, 'Maximum 100 admins per batch'),
}); });
export const bulkReinstateAdminSchema = z.object({ export const bulkReinstateAdminSchema = z.object({
auth0Subs: z.array(z.string().min(1, 'auth0Sub cannot be empty')) ids: z.array(z.string().uuid('Invalid admin ID format'))
.min(1, 'At least one auth0Sub must be provided') .min(1, 'At least one admin ID must be provided')
.max(100, 'Maximum 100 admins per batch'), .max(100, 'Maximum 100 admins per batch'),
}); });
@@ -49,7 +49,7 @@ export const bulkDeleteCatalogSchema = z.object({
}); });
export type CreateAdminInput = z.infer<typeof createAdminSchema>; export type CreateAdminInput = z.infer<typeof createAdminSchema>;
export type AdminAuth0SubInput = z.infer<typeof adminAuth0SubSchema>; export type AdminIdInput = z.infer<typeof adminIdSchema>;
export type AuditLogsQueryInput = z.infer<typeof auditLogsQuerySchema>; export type AuditLogsQueryInput = z.infer<typeof auditLogsQuerySchema>;
export type BulkCreateAdminInput = z.infer<typeof bulkCreateAdminSchema>; export type BulkCreateAdminInput = z.infer<typeof bulkCreateAdminSchema>;
export type BulkRevokeAdminInput = z.infer<typeof bulkRevokeAdminSchema>; export type BulkRevokeAdminInput = z.infer<typeof bulkRevokeAdminSchema>;

View File

@@ -14,13 +14,13 @@ import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger'; import { logger } from '../../../core/logging/logger';
import { import {
listUsersQuerySchema, listUsersQuerySchema,
userAuth0SubSchema, userIdSchema,
updateTierSchema, updateTierSchema,
deactivateUserSchema, deactivateUserSchema,
updateProfileSchema, updateProfileSchema,
promoteToAdminSchema, promoteToAdminSchema,
ListUsersQueryInput, ListUsersQueryInput,
UserAuth0SubInput, UserIdInput,
UpdateTierInput, UpdateTierInput,
DeactivateUserInput, DeactivateUserInput,
UpdateProfileInput, 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( async getUserVehicles(
request: FastifyRequest<{ Params: UserAuth0SubInput }>, request: FastifyRequest<{ Params: UserIdInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
@@ -119,7 +119,7 @@ export class UsersController {
} }
// Validate path param // Validate path param
const parseResult = userAuth0SubSchema.safeParse(request.params); const parseResult = userIdSchema.safeParse(request.params);
if (!parseResult.success) { if (!parseResult.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Validation error', error: 'Validation error',
@@ -127,14 +127,14 @@ export class UsersController {
}); });
} }
const { auth0Sub } = parseResult.data; const { userId } = parseResult.data;
const vehicles = await this.userProfileRepository.getUserVehiclesForAdmin(auth0Sub); const vehicles = await this.userProfileRepository.getUserVehiclesForAdmin(userId);
return reply.code(200).send({ vehicles }); return reply.code(200).send({ vehicles });
} catch (error) { } catch (error) {
logger.error('Error getting user vehicles', { logger.error('Error getting user vehicles', {
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
auth0Sub: request.params?.auth0Sub, userId: (request.params as any)?.userId,
}); });
return reply.code(500).send({ 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( async getUser(
request: FastifyRequest<{ Params: UserAuth0SubInput }>, request: FastifyRequest<{ Params: UserIdInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
@@ -202,7 +202,7 @@ export class UsersController {
} }
// Validate path param // Validate path param
const parseResult = userAuth0SubSchema.safeParse(request.params); const parseResult = userIdSchema.safeParse(request.params);
if (!parseResult.success) { if (!parseResult.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Validation error', error: 'Validation error',
@@ -210,8 +210,8 @@ export class UsersController {
}); });
} }
const { auth0Sub } = parseResult.data; const { userId } = parseResult.data;
const user = await this.userProfileService.getUserDetails(auth0Sub); const user = await this.userProfileService.getUserDetails(userId);
if (!user) { if (!user) {
return reply.code(404).send({ return reply.code(404).send({
@@ -224,7 +224,7 @@ export class UsersController {
} catch (error) { } catch (error) {
logger.error('Error getting user details', { logger.error('Error getting user details', {
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
auth0Sub: request.params?.auth0Sub, userId: (request.params as any)?.userId,
}); });
return reply.code(500).send({ 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 * Uses subscriptionsService.adminOverrideTier() to sync both subscriptions.tier
* and user_profiles.subscription_tier atomically * and user_profiles.subscription_tier atomically
*/ */
async updateTier( async updateTier(
request: FastifyRequest<{ Params: UserAuth0SubInput; Body: UpdateTierInput }>, request: FastifyRequest<{ Params: UserIdInput; Body: UpdateTierInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
@@ -253,7 +253,7 @@ export class UsersController {
} }
// Validate path param // Validate path param
const paramsResult = userAuth0SubSchema.safeParse(request.params); const paramsResult = userIdSchema.safeParse(request.params);
if (!paramsResult.success) { if (!paramsResult.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Validation error', error: 'Validation error',
@@ -270,11 +270,11 @@ export class UsersController {
}); });
} }
const { auth0Sub } = paramsResult.data; const { userId } = paramsResult.data;
const { subscriptionTier } = bodyResult.data; const { subscriptionTier } = bodyResult.data;
// Verify user exists before attempting tier change // Verify user exists before attempting tier change
const currentUser = await this.userProfileService.getUserDetails(auth0Sub); const currentUser = await this.userProfileService.getUserDetails(userId);
if (!currentUser) { if (!currentUser) {
return reply.code(404).send({ return reply.code(404).send({
error: 'Not found', error: 'Not found',
@@ -285,34 +285,34 @@ export class UsersController {
const previousTier = currentUser.subscriptionTier; const previousTier = currentUser.subscriptionTier;
// Use subscriptionsService to update both tables atomically // Use subscriptionsService to update both tables atomically
await this.subscriptionsService.adminOverrideTier(auth0Sub, subscriptionTier); await this.subscriptionsService.adminOverrideTier(userId, subscriptionTier);
// Log audit action // Log audit action
await this.adminRepository.logAuditAction( await this.adminRepository.logAuditAction(
actorId, actorId,
'UPDATE_TIER', 'UPDATE_TIER',
auth0Sub, userId,
'user_profile', 'user_profile',
currentUser.id, currentUser.id,
{ previousTier, newTier: subscriptionTier } { previousTier, newTier: subscriptionTier }
); );
logger.info('User subscription tier updated via admin', { logger.info('User subscription tier updated via admin', {
auth0Sub, userId,
previousTier, previousTier,
newTier: subscriptionTier, newTier: subscriptionTier,
actorAuth0Sub: actorId, actorId,
}); });
// Return updated user profile // Return updated user profile
const updatedUser = await this.userProfileService.getUserDetails(auth0Sub); const updatedUser = await this.userProfileService.getUserDetails(userId);
return reply.code(200).send(updatedUser); return reply.code(200).send(updatedUser);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('Error updating user tier', { logger.error('Error updating user tier', {
error: errorMessage, error: errorMessage,
auth0Sub: request.params?.auth0Sub, userId: (request.params as any)?.userId,
}); });
if (errorMessage === 'User not found') { 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( async deactivateUser(
request: FastifyRequest<{ Params: UserAuth0SubInput; Body: DeactivateUserInput }>, request: FastifyRequest<{ Params: UserIdInput; Body: DeactivateUserInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
@@ -346,7 +346,7 @@ export class UsersController {
} }
// Validate path param // Validate path param
const paramsResult = userAuth0SubSchema.safeParse(request.params); const paramsResult = userIdSchema.safeParse(request.params);
if (!paramsResult.success) { if (!paramsResult.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Validation error', error: 'Validation error',
@@ -363,11 +363,11 @@ export class UsersController {
}); });
} }
const { auth0Sub } = paramsResult.data; const { userId } = paramsResult.data;
const { reason } = bodyResult.data; const { reason } = bodyResult.data;
const deactivatedUser = await this.userProfileService.deactivateUser( const deactivatedUser = await this.userProfileService.deactivateUser(
auth0Sub, userId,
actorId, actorId,
reason reason
); );
@@ -378,7 +378,7 @@ export class UsersController {
logger.error('Error deactivating user', { logger.error('Error deactivating user', {
error: errorMessage, error: errorMessage,
auth0Sub: request.params?.auth0Sub, userId: (request.params as any)?.userId,
}); });
if (errorMessage === 'User not found') { 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( async reactivateUser(
request: FastifyRequest<{ Params: UserAuth0SubInput }>, request: FastifyRequest<{ Params: UserIdInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
@@ -426,7 +426,7 @@ export class UsersController {
} }
// Validate path param // Validate path param
const paramsResult = userAuth0SubSchema.safeParse(request.params); const paramsResult = userIdSchema.safeParse(request.params);
if (!paramsResult.success) { if (!paramsResult.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Validation error', error: 'Validation error',
@@ -434,10 +434,10 @@ export class UsersController {
}); });
} }
const { auth0Sub } = paramsResult.data; const { userId } = paramsResult.data;
const reactivatedUser = await this.userProfileService.reactivateUser( const reactivatedUser = await this.userProfileService.reactivateUser(
auth0Sub, userId,
actorId actorId
); );
@@ -447,7 +447,7 @@ export class UsersController {
logger.error('Error reactivating user', { logger.error('Error reactivating user', {
error: errorMessage, error: errorMessage,
auth0Sub: request.params?.auth0Sub, userId: (request.params as any)?.userId,
}); });
if (errorMessage === 'User not found') { 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( async updateProfile(
request: FastifyRequest<{ Params: UserAuth0SubInput; Body: UpdateProfileInput }>, request: FastifyRequest<{ Params: UserIdInput; Body: UpdateProfileInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
@@ -488,7 +488,7 @@ export class UsersController {
} }
// Validate path param // Validate path param
const paramsResult = userAuth0SubSchema.safeParse(request.params); const paramsResult = userIdSchema.safeParse(request.params);
if (!paramsResult.success) { if (!paramsResult.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Validation error', error: 'Validation error',
@@ -505,11 +505,11 @@ export class UsersController {
}); });
} }
const { auth0Sub } = paramsResult.data; const { userId } = paramsResult.data;
const updates = bodyResult.data; const updates = bodyResult.data;
const updatedUser = await this.userProfileService.adminUpdateProfile( const updatedUser = await this.userProfileService.adminUpdateProfile(
auth0Sub, userId,
updates, updates,
actorId actorId
); );
@@ -520,7 +520,7 @@ export class UsersController {
logger.error('Error updating user profile', { logger.error('Error updating user profile', {
error: errorMessage, error: errorMessage,
auth0Sub: request.params?.auth0Sub, userId: (request.params as any)?.userId,
}); });
if (errorMessage === 'User not found') { 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( async promoteToAdmin(
request: FastifyRequest<{ Params: UserAuth0SubInput; Body: PromoteToAdminInput }>, request: FastifyRequest<{ Params: UserIdInput; Body: PromoteToAdminInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
@@ -554,7 +554,7 @@ export class UsersController {
} }
// Validate path param // Validate path param
const paramsResult = userAuth0SubSchema.safeParse(request.params); const paramsResult = userIdSchema.safeParse(request.params);
if (!paramsResult.success) { if (!paramsResult.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Validation error', error: 'Validation error',
@@ -571,11 +571,11 @@ export class UsersController {
}); });
} }
const { auth0Sub } = paramsResult.data; const { userId } = paramsResult.data;
const { role } = bodyResult.data; const { role } = bodyResult.data;
// Get the user profile first to verify they exist and get their email // Get the user profile to verify they exist and get their email
const user = await this.userProfileService.getUserDetails(auth0Sub); const user = await this.userProfileService.getUserDetails(userId);
if (!user) { if (!user) {
return reply.code(404).send({ return reply.code(404).send({
error: 'Not found', 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( const adminUser = await this.adminService.createAdmin(
user.email, user.email,
role, role,
auth0Sub, // Use the real auth0Sub from the user profile userId,
actorId actorAdmin?.id || actorId
); );
return reply.code(201).send(adminUser); return reply.code(201).send(adminUser);
@@ -605,7 +608,7 @@ export class UsersController {
logger.error('Error promoting user to admin', { logger.error('Error promoting user to admin', {
error: errorMessage, error: errorMessage,
auth0Sub: request.params?.auth0Sub, userId: (request.params as any)?.userId,
}); });
if (errorMessage.includes('already exists')) { 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( async hardDeleteUser(
request: FastifyRequest<{ Params: UserAuth0SubInput }>, request: FastifyRequest<{ Params: UserIdInput }>,
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
@@ -639,7 +642,7 @@ export class UsersController {
} }
// Validate path param // Validate path param
const paramsResult = userAuth0SubSchema.safeParse(request.params); const paramsResult = userIdSchema.safeParse(request.params);
if (!paramsResult.success) { if (!paramsResult.success) {
return reply.code(400).send({ return reply.code(400).send({
error: 'Validation error', error: 'Validation error',
@@ -647,14 +650,14 @@ export class UsersController {
}); });
} }
const { auth0Sub } = paramsResult.data; const { userId } = paramsResult.data;
// Optional reason from query params // Optional reason from query params
const reason = (request.query as any)?.reason; const reason = (request.query as any)?.reason;
// Hard delete user // Hard delete user
await this.userProfileService.adminHardDeleteUser( await this.userProfileService.adminHardDeleteUser(
auth0Sub, userId,
actorId, actorId,
reason reason
); );
@@ -667,7 +670,7 @@ export class UsersController {
logger.error('Error hard deleting user', { logger.error('Error hard deleting user', {
error: errorMessage, error: errorMessage,
auth0Sub: request.params?.auth0Sub, userId: (request.params as any)?.userId,
}); });
if (errorMessage === 'Cannot delete your own account') { if (errorMessage === 'Cannot delete your own account') {

View File

@@ -19,9 +19,9 @@ export const listUsersQuerySchema = z.object({
sortOrder: z.enum(['asc', 'desc']).default('desc'), sortOrder: z.enum(['asc', 'desc']).default('desc'),
}); });
// Path param for user auth0Sub // Path param for user UUID
export const userAuth0SubSchema = z.object({ export const userIdSchema = z.object({
auth0Sub: z.string().min(1, 'auth0Sub is required'), userId: z.string().uuid('Invalid user ID format'),
}); });
// Body for updating subscription tier // Body for updating subscription tier
@@ -50,7 +50,7 @@ export const promoteToAdminSchema = z.object({
// Type exports // Type exports
export type ListUsersQueryInput = z.infer<typeof listUsersQuerySchema>; export type ListUsersQueryInput = z.infer<typeof listUsersQuerySchema>;
export type UserAuth0SubInput = z.infer<typeof userAuth0SubSchema>; export type UserIdInput = z.infer<typeof userIdSchema>;
export type UpdateTierInput = z.infer<typeof updateTierSchema>; export type UpdateTierInput = z.infer<typeof updateTierSchema>;
export type DeactivateUserInput = z.infer<typeof deactivateUserSchema>; export type DeactivateUserInput = z.infer<typeof deactivateUserSchema>;
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>; export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;

View File

@@ -10,29 +10,49 @@ import { logger } from '../../../core/logging/logger';
export class AdminRepository { export class AdminRepository {
constructor(private pool: Pool) {} constructor(private pool: Pool) {}
async getAdminByAuth0Sub(auth0Sub: string): Promise<AdminUser | null> { async getAdminById(id: string): Promise<AdminUser | null> {
const query = ` 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 FROM admin_users
WHERE auth0_sub = $1 WHERE id = $1
LIMIT 1 LIMIT 1
`; `;
try { try {
const result = await this.pool.query(query, [auth0Sub]); const result = await this.pool.query(query, [id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
return null; return null;
} }
return this.mapRowToAdminUser(result.rows[0]); return this.mapRowToAdminUser(result.rows[0]);
} catch (error) { } 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<AdminUser | null> {
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; throw error;
} }
} }
async getAdminByEmail(email: string): Promise<AdminUser | null> { async getAdminByEmail(email: string): Promise<AdminUser | null> {
const query = ` 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 FROM admin_users
WHERE LOWER(email) = LOWER($1) WHERE LOWER(email) = LOWER($1)
LIMIT 1 LIMIT 1
@@ -52,7 +72,7 @@ export class AdminRepository {
async getAllAdmins(): Promise<AdminUser[]> { async getAllAdmins(): Promise<AdminUser[]> {
const query = ` 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 FROM admin_users
ORDER BY created_at DESC ORDER BY created_at DESC
`; `;
@@ -68,7 +88,7 @@ export class AdminRepository {
async getActiveAdmins(): Promise<AdminUser[]> { async getActiveAdmins(): Promise<AdminUser[]> {
const query = ` 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 FROM admin_users
WHERE revoked_at IS NULL WHERE revoked_at IS NULL
ORDER BY created_at DESC ORDER BY created_at DESC
@@ -83,61 +103,61 @@ export class AdminRepository {
} }
} }
async createAdmin(auth0Sub: string, email: string, role: string, createdBy: string): Promise<AdminUser> { async createAdmin(userProfileId: string, email: string, role: string, createdBy: string): Promise<AdminUser> {
const query = ` 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) 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 { 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) { if (result.rows.length === 0) {
throw new Error('Failed to create admin user'); throw new Error('Failed to create admin user');
} }
return this.mapRowToAdminUser(result.rows[0]); return this.mapRowToAdminUser(result.rows[0]);
} catch (error) { } catch (error) {
logger.error('Error creating admin', { error, auth0Sub, email }); logger.error('Error creating admin', { error, userProfileId, email });
throw error; throw error;
} }
} }
async revokeAdmin(auth0Sub: string): Promise<AdminUser> { async revokeAdmin(id: string): Promise<AdminUser> {
const query = ` const query = `
UPDATE admin_users UPDATE admin_users
SET revoked_at = CURRENT_TIMESTAMP SET revoked_at = CURRENT_TIMESTAMP
WHERE auth0_sub = $1 WHERE id = $1
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 { try {
const result = await this.pool.query(query, [auth0Sub]); const result = await this.pool.query(query, [id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
throw new Error('Admin user not found'); throw new Error('Admin user not found');
} }
return this.mapRowToAdminUser(result.rows[0]); return this.mapRowToAdminUser(result.rows[0]);
} catch (error) { } catch (error) {
logger.error('Error revoking admin', { error, auth0Sub }); logger.error('Error revoking admin', { error, id });
throw error; throw error;
} }
} }
async reinstateAdmin(auth0Sub: string): Promise<AdminUser> { async reinstateAdmin(id: string): Promise<AdminUser> {
const query = ` const query = `
UPDATE admin_users UPDATE admin_users
SET revoked_at = NULL SET revoked_at = NULL
WHERE auth0_sub = $1 WHERE id = $1
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 { try {
const result = await this.pool.query(query, [auth0Sub]); const result = await this.pool.query(query, [id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
throw new Error('Admin user not found'); throw new Error('Admin user not found');
} }
return this.mapRowToAdminUser(result.rows[0]); return this.mapRowToAdminUser(result.rows[0]);
} catch (error) { } catch (error) {
logger.error('Error reinstating admin', { error, auth0Sub }); logger.error('Error reinstating admin', { error, id });
throw error; throw error;
} }
} }
@@ -202,30 +222,11 @@ export class AdminRepository {
} }
} }
async updateAuth0SubByEmail(email: string, auth0Sub: string): Promise<AdminUser> {
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 { private mapRowToAdminUser(row: any): AdminUser {
return { return {
auth0Sub: row.auth0_sub, id: row.id,
userProfileId: row.user_profile_id,
email: row.email, email: row.email,
role: row.role, role: row.role,
createdAt: new Date(row.created_at), createdAt: new Date(row.created_at),

View File

@@ -11,11 +11,20 @@ import { auditLogService } from '../../audit-log';
export class AdminService { export class AdminService {
constructor(private repository: AdminRepository) {} constructor(private repository: AdminRepository) {}
async getAdminByAuth0Sub(auth0Sub: string): Promise<AdminUser | null> { async getAdminById(id: string): Promise<AdminUser | null> {
try { try {
return await this.repository.getAdminByAuth0Sub(auth0Sub); return await this.repository.getAdminById(id);
} catch (error) { } 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<AdminUser | null> {
try {
return await this.repository.getAdminByUserProfileId(userProfileId);
} catch (error) {
logger.error('Error getting admin by user_profile_id', { error });
throw error; throw error;
} }
} }
@@ -47,7 +56,7 @@ export class AdminService {
} }
} }
async createAdmin(email: string, role: string, auth0Sub: string, createdBy: string): Promise<AdminUser> { async createAdmin(email: string, role: string, userProfileId: string, createdByAdminId: string): Promise<AdminUser> {
try { try {
// Check if admin already exists // Check if admin already exists
const normalizedEmail = email.trim().toLowerCase(); const normalizedEmail = email.trim().toLowerCase();
@@ -57,10 +66,10 @@ export class AdminService {
} }
// Create new admin // 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) // 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, email,
role role
}); });
@@ -68,10 +77,10 @@ export class AdminService {
// Log to unified audit log // Log to unified audit log
await auditLogService.info( await auditLogService.info(
'admin', 'admin',
createdBy, userProfileId,
`Admin user created: ${admin.email}`, `Admin user created: ${admin.email}`,
'admin_user', 'admin_user',
admin.auth0Sub, admin.id,
{ email: admin.email, role } { email: admin.email, role }
).catch(err => logger.error('Failed to log admin create audit event', { error: err })); ).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<AdminUser> { async revokeAdmin(id: string, revokedByAdminId: string): Promise<AdminUser> {
try { try {
// Check that at least one active admin will remain // Check that at least one active admin will remain
const activeAdmins = await this.repository.getActiveAdmins(); const activeAdmins = await this.repository.getActiveAdmins();
@@ -92,51 +101,51 @@ export class AdminService {
} }
// Revoke the admin // Revoke the admin
const admin = await this.repository.revokeAdmin(auth0Sub); const admin = await this.repository.revokeAdmin(id);
// Log audit action (legacy) // 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 // Log to unified audit log
await auditLogService.info( await auditLogService.info(
'admin', 'admin',
revokedBy, admin.userProfileId,
`Admin user revoked: ${admin.email}`, `Admin user revoked: ${admin.email}`,
'admin_user', 'admin_user',
auth0Sub, id,
{ email: admin.email } { email: admin.email }
).catch(err => logger.error('Failed to log admin revoke audit event', { error: err })); ).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; return admin;
} catch (error) { } catch (error) {
logger.error('Error revoking admin', { error, auth0Sub }); logger.error('Error revoking admin', { error, id });
throw error; throw error;
} }
} }
async reinstateAdmin(auth0Sub: string, reinstatedBy: string): Promise<AdminUser> { async reinstateAdmin(id: string, reinstatedByAdminId: string): Promise<AdminUser> {
try { try {
// Reinstate the admin // Reinstate the admin
const admin = await this.repository.reinstateAdmin(auth0Sub); const admin = await this.repository.reinstateAdmin(id);
// Log audit action (legacy) // 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 // Log to unified audit log
await auditLogService.info( await auditLogService.info(
'admin', 'admin',
reinstatedBy, admin.userProfileId,
`Admin user reinstated: ${admin.email}`, `Admin user reinstated: ${admin.email}`,
'admin_user', 'admin_user',
auth0Sub, id,
{ email: admin.email } { email: admin.email }
).catch(err => logger.error('Failed to log admin reinstate audit event', { error: err })); ).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; return admin;
} catch (error) { } catch (error) {
logger.error('Error reinstating admin', { error, auth0Sub }); logger.error('Error reinstating admin', { error, id });
throw error; throw error;
} }
} }
@@ -150,12 +159,4 @@ export class AdminService {
} }
} }
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;
}
}
} }

View File

@@ -4,7 +4,8 @@
*/ */
export interface AdminUser { export interface AdminUser {
auth0Sub: string; id: string;
userProfileId: string;
email: string; email: string;
role: 'admin' | 'super_admin'; role: 'admin' | 'super_admin';
createdAt: Date; createdAt: Date;
@@ -19,11 +20,11 @@ export interface CreateAdminRequest {
} }
export interface RevokeAdminRequest { export interface RevokeAdminRequest {
auth0Sub: string; id: string;
} }
export interface ReinstateAdminRequest { export interface ReinstateAdminRequest {
auth0Sub: string; id: string;
} }
export interface AdminAuditLog { export interface AdminAuditLog {
@@ -71,25 +72,25 @@ export interface BulkCreateAdminResponse {
} }
export interface BulkRevokeAdminRequest { export interface BulkRevokeAdminRequest {
auth0Subs: string[]; ids: string[];
} }
export interface BulkRevokeAdminResponse { export interface BulkRevokeAdminResponse {
revoked: AdminUser[]; revoked: AdminUser[];
failed: Array<{ failed: Array<{
auth0Sub: string; id: string;
error: string; error: string;
}>; }>;
} }
export interface BulkReinstateAdminRequest { export interface BulkReinstateAdminRequest {
auth0Subs: string[]; ids: string[];
} }
export interface BulkReinstateAdminResponse { export interface BulkReinstateAdminResponse {
reinstated: AdminUser[]; reinstated: AdminUser[];
failed: Array<{ failed: Array<{
auth0Sub: string; id: string;
error: string; error: string;
}>; }>;
} }