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:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user