Admin Page work - Still blank/broken

This commit is contained in:
Eric Gullickson
2025-11-06 16:29:11 -06:00
parent 858cf31d38
commit 5630979adf
38 changed files with 7373 additions and 924 deletions

View File

@@ -11,13 +11,25 @@ import { logger } from '../../../core/logging/logger';
import {
CreateAdminInput,
AdminAuth0SubInput,
AuditLogsQueryInput
AuditLogsQueryInput,
BulkCreateAdminInput,
BulkRevokeAdminInput,
BulkReinstateAdminInput
} from './admin.validation';
import {
createAdminSchema,
adminAuth0SubSchema,
auditLogsQuerySchema
auditLogsQuerySchema,
bulkCreateAdminSchema,
bulkRevokeAdminSchema,
bulkReinstateAdminSchema
} from './admin.validation';
import {
BulkCreateAdminResponse,
BulkRevokeAdminResponse,
BulkReinstateAdminResponse,
AdminUser
} from '../domain/admin.types';
export class AdminController {
private adminService: AdminService;
@@ -398,6 +410,260 @@ export class AdminController {
}
}
/**
* POST /api/admin/admins/bulk - Create multiple admin users
*/
async bulkCreateAdmins(
request: FastifyRequest<{ Body: BulkCreateAdminInput }>,
reply: FastifyReply
) {
try {
const actorId = request.userContext?.userId;
if (!actorId) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'User context missing'
});
}
// Validate request body
const validation = bulkCreateAdminSchema.safeParse(request.body);
if (!validation.success) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Invalid request body',
details: validation.error.errors
});
}
const { admins } = validation.data;
const created: AdminUser[] = [];
const failed: Array<{ email: string; error: string }> = [];
// Process each admin creation sequentially to maintain data consistency
for (const adminInput of admins) {
try {
const { email, role = 'admin' } = adminInput;
// Generate auth0Sub for the new admin
// In production, this should be the actual Auth0 user ID
const auth0Sub = `auth0|${email.replace('@', '_at_')}`;
const admin = await this.adminService.createAdmin(
email,
role,
auth0Sub,
actorId
);
created.push(admin);
} catch (error: any) {
logger.error('Error creating admin in bulk operation', {
error: error.message,
email: adminInput.email,
actorId
});
failed.push({
email: adminInput.email,
error: error.message || 'Failed to create admin'
});
}
}
const response: BulkCreateAdminResponse = {
created,
failed
};
// Return 207 Multi-Status if there were any failures, 201 if all succeeded
const statusCode = failed.length > 0 ? 207 : 201;
return reply.code(statusCode).send(response);
} catch (error: any) {
logger.error('Error in bulk create admins', {
error: error.message,
actorId: request.userContext?.userId
});
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to process bulk admin creation'
});
}
}
/**
* PATCH /api/admin/admins/bulk-revoke - Revoke multiple admin users
*/
async bulkRevokeAdmins(
request: FastifyRequest<{ Body: BulkRevokeAdminInput }>,
reply: FastifyReply
) {
try {
const actorId = request.userContext?.userId;
if (!actorId) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'User context missing'
});
}
// Validate request body
const validation = bulkRevokeAdminSchema.safeParse(request.body);
if (!validation.success) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Invalid request body',
details: validation.error.errors
});
}
const { auth0Subs } = validation.data;
const revoked: AdminUser[] = [];
const failed: Array<{ auth0Sub: string; error: string }> = [];
// Process each revocation sequentially to maintain data consistency
for (const auth0Sub of auth0Subs) {
try {
// Check if admin exists
const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub);
if (!targetAdmin) {
failed.push({
auth0Sub,
error: 'Admin user not found'
});
continue;
}
// Attempt to revoke the admin
const admin = await this.adminService.revokeAdmin(auth0Sub, actorId);
revoked.push(admin);
} catch (error: any) {
logger.error('Error revoking admin in bulk operation', {
error: error.message,
auth0Sub,
actorId
});
// Special handling for "last admin" constraint
failed.push({
auth0Sub,
error: error.message || 'Failed to revoke admin'
});
}
}
const response: BulkRevokeAdminResponse = {
revoked,
failed
};
// Return 207 Multi-Status if there were any failures, 200 if all succeeded
const statusCode = failed.length > 0 ? 207 : 200;
return reply.code(statusCode).send(response);
} catch (error: any) {
logger.error('Error in bulk revoke admins', {
error: error.message,
actorId: request.userContext?.userId
});
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to process bulk admin revocation'
});
}
}
/**
* PATCH /api/admin/admins/bulk-reinstate - Reinstate multiple revoked admin users
*/
async bulkReinstateAdmins(
request: FastifyRequest<{ Body: BulkReinstateAdminInput }>,
reply: FastifyReply
) {
try {
const actorId = request.userContext?.userId;
if (!actorId) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'User context missing'
});
}
// Validate request body
const validation = bulkReinstateAdminSchema.safeParse(request.body);
if (!validation.success) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Invalid request body',
details: validation.error.errors
});
}
const { auth0Subs } = validation.data;
const reinstated: AdminUser[] = [];
const failed: Array<{ auth0Sub: string; error: string }> = [];
// Process each reinstatement sequentially to maintain data consistency
for (const auth0Sub of auth0Subs) {
try {
// Check if admin exists
const targetAdmin = await this.adminService.getAdminByAuth0Sub(auth0Sub);
if (!targetAdmin) {
failed.push({
auth0Sub,
error: 'Admin user not found'
});
continue;
}
// Attempt to reinstate the admin
const admin = await this.adminService.reinstateAdmin(auth0Sub, actorId);
reinstated.push(admin);
} catch (error: any) {
logger.error('Error reinstating admin in bulk operation', {
error: error.message,
auth0Sub,
actorId
});
failed.push({
auth0Sub,
error: error.message || 'Failed to reinstate admin'
});
}
}
const response: BulkReinstateAdminResponse = {
reinstated,
failed
};
// Return 207 Multi-Status if there were any failures, 200 if all succeeded
const statusCode = failed.length > 0 ? 207 : 200;
return reply.code(statusCode).send(response);
} catch (error: any) {
logger.error('Error in bulk reinstate admins', {
error: error.message,
actorId: request.userContext?.userId
});
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to process bulk admin reinstatement'
});
}
}
private resolveUserEmail(request: FastifyRequest): string | undefined {
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));

View File

@@ -8,7 +8,12 @@ import { AdminController } from './admin.controller';
import {
CreateAdminInput,
AdminAuth0SubInput,
AuditLogsQueryInput
AuditLogsQueryInput,
BulkCreateAdminInput,
BulkRevokeAdminInput,
BulkReinstateAdminInput,
BulkDeleteCatalogInput,
CatalogEntity
} from './admin.validation';
import { AdminRepository } from '../data/admin.repository';
import { StationOversightService } from '../domain/station-oversight.service';
@@ -69,6 +74,24 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => {
handler: adminController.getAuditLogs.bind(adminController)
});
// POST /api/admin/admins/bulk - Create multiple admins
fastify.post<{ Body: BulkCreateAdminInput }>('/admin/admins/bulk', {
preHandler: [fastify.requireAdmin],
handler: adminController.bulkCreateAdmins.bind(adminController)
});
// PATCH /api/admin/admins/bulk-revoke - Revoke multiple admins
fastify.patch<{ Body: BulkRevokeAdminInput }>('/admin/admins/bulk-revoke', {
preHandler: [fastify.requireAdmin],
handler: adminController.bulkRevokeAdmins.bind(adminController)
});
// PATCH /api/admin/admins/bulk-reinstate - Reinstate multiple admins
fastify.patch<{ Body: BulkReinstateAdminInput }>('/admin/admins/bulk-reinstate', {
preHandler: [fastify.requireAdmin],
handler: adminController.bulkReinstateAdmins.bind(adminController)
});
// Phase 3: Catalog CRUD endpoints
// Makes endpoints
@@ -182,6 +205,12 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => {
handler: catalogController.getChangeLogs.bind(catalogController)
});
// Bulk delete endpoint
fastify.delete<{ Params: { entity: CatalogEntity }; Body: BulkDeleteCatalogInput }>('/admin/catalog/:entity/bulk-delete', {
preHandler: [fastify.requireAdmin],
handler: catalogController.bulkDeleteCatalogEntity.bind(catalogController)
});
// Phase 4: Station oversight endpoints
// GET /api/admin/stations - List all stations globally

View File

@@ -19,6 +19,40 @@ export const auditLogsQuerySchema = z.object({
offset: z.coerce.number().min(0).default(0),
});
export const bulkCreateAdminSchema = z.object({
admins: z.array(
z.object({
email: z.string().email('Invalid email format'),
role: z.enum(['admin', 'super_admin']).optional().default('admin'),
})
).min(1, 'At least one admin must be provided').max(100, 'Maximum 100 admins per batch'),
});
export const bulkRevokeAdminSchema = z.object({
auth0Subs: z.array(z.string().min(1, 'auth0Sub cannot be empty'))
.min(1, 'At least one auth0Sub must be provided')
.max(100, 'Maximum 100 admins per batch'),
});
export const bulkReinstateAdminSchema = z.object({
auth0Subs: z.array(z.string().min(1, 'auth0Sub cannot be empty'))
.min(1, 'At least one auth0Sub must be provided')
.max(100, 'Maximum 100 admins per batch'),
});
export const catalogEntitySchema = z.enum(['makes', 'models', 'years', 'trims', 'engines']);
export const bulkDeleteCatalogSchema = z.object({
ids: z.array(z.number().int().positive('ID must be a positive integer'))
.min(1, 'At least one ID must be provided')
.max(100, 'Maximum 100 items per batch'),
});
export type CreateAdminInput = z.infer<typeof createAdminSchema>;
export type AdminAuth0SubInput = z.infer<typeof adminAuth0SubSchema>;
export type AuditLogsQueryInput = z.infer<typeof auditLogsQuerySchema>;
export type BulkCreateAdminInput = z.infer<typeof bulkCreateAdminSchema>;
export type BulkRevokeAdminInput = z.infer<typeof bulkRevokeAdminSchema>;
export type BulkReinstateAdminInput = z.infer<typeof bulkReinstateAdminSchema>;
export type CatalogEntity = z.infer<typeof catalogEntitySchema>;
export type BulkDeleteCatalogInput = z.infer<typeof bulkDeleteCatalogSchema>;

View File

@@ -536,4 +536,103 @@ export class CatalogController {
reply.code(500).send({ error: 'Failed to retrieve change logs' });
}
}
// BULK DELETE ENDPOINT
async bulkDeleteCatalogEntity(
request: FastifyRequest<{ Params: { entity: string }; Body: { ids: number[] } }>,
reply: FastifyReply
): Promise<void> {
try {
const { entity } = request.params;
const { ids } = request.body;
const actorId = request.userContext?.userId || 'unknown';
// Validate entity type
const validEntities = ['makes', 'models', 'years', 'trims', 'engines'];
if (!validEntities.includes(entity)) {
reply.code(400).send({
error: 'Invalid entity type',
message: `Entity must be one of: ${validEntities.join(', ')}`
});
return;
}
// Validate IDs are provided
if (!ids || !Array.isArray(ids) || ids.length === 0) {
reply.code(400).send({
error: 'Invalid request',
message: 'At least one ID must be provided'
});
return;
}
// Validate all IDs are valid integers
const invalidIds = ids.filter(id => !Number.isInteger(id) || id <= 0);
if (invalidIds.length > 0) {
reply.code(400).send({
error: 'Invalid IDs',
message: 'All IDs must be positive integers'
});
return;
}
const deleted: number[] = [];
const failed: Array<{ id: number; error: string }> = [];
// Map entity to delete method
const deleteMethodMap: Record<string, (id: number, actorId: string) => Promise<void>> = {
makes: (id, actor) => this.catalogService.deleteMake(id, actor),
models: (id, actor) => this.catalogService.deleteModel(id, actor),
years: (id, actor) => this.catalogService.deleteYear(id, actor),
trims: (id, actor) => this.catalogService.deleteTrim(id, actor),
engines: (id, actor) => this.catalogService.deleteEngine(id, actor)
};
const deleteMethod = deleteMethodMap[entity];
// Process each deletion sequentially to maintain data consistency
for (const id of ids) {
try {
await deleteMethod(id, actorId);
deleted.push(id);
} catch (error: any) {
logger.error(`Error deleting ${entity} in bulk operation`, {
error: error.message,
entity,
id,
actorId
});
failed.push({
id,
error: error.message || `Failed to delete ${entity}`
});
}
}
const response = {
deleted,
failed
};
// Return 207 Multi-Status if there were any failures, 204 if all succeeded
if (failed.length > 0) {
reply.code(207).send(response);
} else {
reply.code(204).send();
}
} catch (error: any) {
logger.error('Error in bulk delete catalog entity', {
error: error.message,
entity: request.params.entity,
actorId: request.userContext?.userId
});
reply.code(500).send({
error: 'Internal server error',
message: 'Failed to process bulk deletion'
});
}
}
}

View File

@@ -53,3 +53,51 @@ export interface AdminAuditResponse {
total: number;
logs: AdminAuditLog[];
}
// Batch operation types
export interface BulkCreateAdminRequest {
admins: Array<{
email: string;
role?: 'admin' | 'super_admin';
}>;
}
export interface BulkCreateAdminResponse {
created: AdminUser[];
failed: Array<{
email: string;
error: string;
}>;
}
export interface BulkRevokeAdminRequest {
auth0Subs: string[];
}
export interface BulkRevokeAdminResponse {
revoked: AdminUser[];
failed: Array<{
auth0Sub: string;
error: string;
}>;
}
export interface BulkReinstateAdminRequest {
auth0Subs: string[];
}
export interface BulkReinstateAdminResponse {
reinstated: AdminUser[];
failed: Array<{
auth0Sub: string;
error: string;
}>;
}
export interface BulkDeleteCatalogResponse {
deleted: number[];
failed: Array<{
id: number;
error: string;
}>;
}