Admin Page work - Still blank/broken
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user