// src/routes/admin.routes.ts import { Router, NextFunction, Request, Response } from 'express'; import passport, { isAdmin } from '../config/passport'; import { z } from 'zod'; import * as db from '../services/db/index.db'; import type { UserProfile } from '../types'; import { geocodingService } from '../services/geocodingService.server'; import { cacheService } from '../services/cacheService.server'; import { requireFileUpload } from '../middleware/fileUpload.middleware'; // This was a duplicate, fixed. import { createUploadMiddleware, handleMulterError } from '../middleware/multer.middleware'; import { ValidationError } from '../services/db/errors.db'; import { validateRequest } from '../middleware/validation.middleware'; // --- Bull Board (Job Queue UI) Imports --- import { createBullBoard } from '@bull-board/api'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; import { ExpressAdapter } from '@bull-board/express'; import { backgroundJobService } from '../services/backgroundJobService'; import { flyerQueue, emailQueue, analyticsQueue, cleanupQueue, weeklyAnalyticsQueue, } from '../services/queueService.server'; import { numericIdParam, uuidParamSchema, optionalNumeric } from '../utils/zodUtils'; // Removed: import { logger } from '../services/logger.server'; // All route handlers now use req.log (request-scoped logger) as per ADR-004 import { monitoringService } from '../services/monitoringService.server'; import { userService } from '../services/userService'; import { cleanupUploadedFile } from '../utils/fileUtils'; import { brandService } from '../services/brandService'; import { adminTriggerLimiter, adminUploadLimiter } from '../config/rateLimiters'; import { sendSuccess, sendNoContent } from '../utils/apiResponse'; const updateCorrectionSchema = numericIdParam('id').extend({ body: z.object({ suggested_value: z.string().trim().min(1, 'A new suggested_value is required.'), }), }); const updateRecipeStatusSchema = numericIdParam('id').extend({ body: z.object({ status: z.enum(['private', 'pending_review', 'public', 'rejected']), }), }); const updateCommentStatusSchema = numericIdParam('id').extend({ body: z.object({ status: z.enum(['visible', 'hidden', 'reported']), }), }); const updateUserRoleSchema = uuidParamSchema('id', 'A valid user ID is required.').extend({ body: z.object({ role: z.enum(['user', 'admin']), }), }); const activityLogSchema = z.object({ query: z.object({ limit: optionalNumeric({ default: 50, integer: true, positive: true }), offset: optionalNumeric({ default: 0, integer: true, nonnegative: true }), }), }); const jobRetrySchema = z.object({ params: z.object({ queueName: z.enum([ 'flyer-processing', 'email-sending', 'analytics-reporting', 'file-cleanup', 'weekly-analytics-reporting', ]), jobId: z.string().trim().min(1, 'A valid Job ID is required.'), }), }); const emptySchema = z.object({}); const router = Router(); const brandLogoUpload = createUploadMiddleware({ storageType: 'flyer', // Using flyer storage path is acceptable for brand logos. fileSize: 2 * 1024 * 1024, // 2MB limit for logos fileFilter: 'image', }); // --- Bull Board (Job Queue UI) Setup --- const serverAdapter = new ExpressAdapter(); serverAdapter.setBasePath('/api/admin/jobs'); // Set the base path for the UI createBullBoard({ queues: [ new BullMQAdapter(flyerQueue), new BullMQAdapter(emailQueue), new BullMQAdapter(analyticsQueue), new BullMQAdapter(cleanupQueue), new BullMQAdapter(weeklyAnalyticsQueue), // Add the weekly analytics queue to the board ], options: { uiConfig: { boardTitle: 'Bull Dashboard', }, }, serverAdapter: serverAdapter, }); // Mount the Bull Board UI router. This must be done BEFORE the isAdmin middleware // so the UI's own assets can be served, but the routes within it will be protected // by the router-level `isAdmin` middleware below. router.use('/jobs', serverAdapter.getRouter()); // --- Middleware for all admin routes --- router.use(passport.authenticate('jwt', { session: false }), isAdmin); // --- Admin Routes --- /** * @openapi * /admin/corrections: * get: * tags: [Admin] * summary: Get suggested corrections * description: Retrieve all suggested corrections for review. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: List of suggested corrections * content: * application/json: * schema: * $ref: '#/components/schemas/SuccessResponse' * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get('/corrections', validateRequest(emptySchema), async (req, res, next: NextFunction) => { try { const corrections = await db.adminRepo.getSuggestedCorrections(req.log); sendSuccess(res, corrections); } catch (error) { req.log.error({ error }, 'Error fetching suggested corrections'); next(error); } }); /** * @openapi * /admin/review/flyers: * get: * tags: [Admin] * summary: Get flyers for review * description: Retrieve flyers pending admin review. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: List of flyers for review * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get('/review/flyers', validateRequest(emptySchema), async (req, res, next: NextFunction) => { try { req.log.debug('Fetching flyers for review via adminRepo'); const flyers = await db.adminRepo.getFlyersForReview(req.log); req.log.info( { count: Array.isArray(flyers) ? flyers.length : 'unknown' }, 'Successfully fetched flyers for review', ); sendSuccess(res, flyers); } catch (error) { req.log.error({ error }, 'Error fetching flyers for review'); next(error); } }); /** * @openapi * /admin/brands: * get: * tags: [Admin] * summary: Get all brands * description: Retrieve all brands. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: List of brands * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get('/brands', validateRequest(emptySchema), async (req, res, next: NextFunction) => { try { const brands = await db.flyerRepo.getAllBrands(req.log); sendSuccess(res, brands); } catch (error) { req.log.error({ error }, 'Error fetching brands'); next(error); } }); /** * @openapi * /admin/stats: * get: * tags: [Admin] * summary: Get application stats * description: Retrieve overall application statistics. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: Application statistics * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get('/stats', validateRequest(emptySchema), async (req, res, next: NextFunction) => { try { const stats = await db.adminRepo.getApplicationStats(req.log); sendSuccess(res, stats); } catch (error) { req.log.error({ error }, 'Error fetching application stats'); next(error); } }); /** * @openapi * /admin/stats/daily: * get: * tags: [Admin] * summary: Get daily statistics * description: Retrieve daily statistics for the last 30 days. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: Daily statistics for last 30 days * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get('/stats/daily', validateRequest(emptySchema), async (req, res, next: NextFunction) => { try { const dailyStats = await db.adminRepo.getDailyStatsForLast30Days(req.log); sendSuccess(res, dailyStats); } catch (error) { req.log.error({ error }, 'Error fetching daily stats'); next(error); } }); /** * @openapi * /admin/corrections/{id}/approve: * post: * tags: [Admin] * summary: Approve a correction * description: Approve a suggested correction. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Correction ID * responses: * 200: * description: Correction approved successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Correction not found */ router.post( '/corrections/:id/approve', validateRequest(numericIdParam('id')), async (req: Request, res: Response, next: NextFunction) => { // Apply ADR-003 pattern for type safety const { params } = req as unknown as z.infer>; try { await db.adminRepo.approveCorrection(params.id, req.log); // params.id is now safely typed as number sendSuccess(res, { message: 'Correction approved successfully.' }); } catch (error) { req.log.error({ error }, 'Error approving correction'); next(error); } }, ); /** * @openapi * /admin/corrections/{id}/reject: * post: * tags: [Admin] * summary: Reject a correction * description: Reject a suggested correction. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Correction ID * responses: * 200: * description: Correction rejected successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Correction not found */ router.post( '/corrections/:id/reject', validateRequest(numericIdParam('id')), async (req: Request, res: Response, next: NextFunction) => { // Apply ADR-003 pattern for type safety const { params } = req as unknown as z.infer>; try { await db.adminRepo.rejectCorrection(params.id, req.log); // params.id is now safely typed as number sendSuccess(res, { message: 'Correction rejected successfully.' }); } catch (error) { req.log.error({ error }, 'Error rejecting correction'); next(error); } }, ); /** * @openapi * /admin/corrections/{id}: * put: * tags: [Admin] * summary: Update a correction * description: Update a suggested correction's value. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Correction ID * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - suggested_value * properties: * suggested_value: * type: string * description: New suggested value * responses: * 200: * description: Correction updated successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Correction not found */ router.put( '/corrections/:id', validateRequest(updateCorrectionSchema), async (req: Request, res: Response, next: NextFunction) => { // Apply ADR-003 pattern for type safety const { params, body } = req as unknown as z.infer; try { const updatedCorrection = await db.adminRepo.updateSuggestedCorrection( params.id, body.suggested_value, req.log, ); sendSuccess(res, updatedCorrection); } catch (error) { req.log.error({ error }, 'Error updating suggested correction'); next(error); } }, ); /** * @openapi * /admin/recipes/{id}/status: * put: * tags: [Admin] * summary: Update recipe status * description: Update a recipe's publication status. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Recipe ID * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - status * properties: * status: * type: string * enum: [private, pending_review, public, rejected] * responses: * 200: * description: Recipe status updated successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Recipe not found */ router.put( '/recipes/:id/status', validateRequest(updateRecipeStatusSchema), async (req: Request, res: Response, next: NextFunction) => { // Apply ADR-003 pattern for type safety const { params, body } = req as unknown as z.infer; try { const updatedRecipe = await db.adminRepo.updateRecipeStatus(params.id, body.status, req.log); // This is still a standalone function in admin.db.ts sendSuccess(res, updatedRecipe); } catch (error) { req.log.error({ error }, 'Error updating recipe status'); next(error); // Pass all errors to the central error handler } }, ); /** * @openapi * /admin/brands/{id}/logo: * post: * tags: [Admin] * summary: Upload brand logo * description: Upload or update a brand's logo image. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Brand ID * requestBody: * required: true * content: * multipart/form-data: * schema: * type: object * required: * - logoImage * properties: * logoImage: * type: string * format: binary * description: Logo image file (max 2MB) * responses: * 200: * description: Brand logo updated successfully * 400: * description: Invalid file or missing logo image * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Brand not found */ router.post( '/brands/:id/logo', adminUploadLimiter, validateRequest(numericIdParam('id')), brandLogoUpload.single('logoImage'), requireFileUpload('logoImage'), async (req: Request, res: Response, next: NextFunction) => { const { params } = req as unknown as z.infer>; try { // Although requireFileUpload middleware should ensure the file exists, // this check satisfies TypeScript and adds robustness. if (!req.file) { throw new ValidationError([], 'Logo image file is missing.'); } const logoUrl = await brandService.updateBrandLogo(params.id, req.file, req.log); req.log.info( { brandId: params.id, logoUrl }, `Brand logo updated for brand ID: ${params.id}`, ); sendSuccess(res, { message: 'Brand logo updated successfully.', logoUrl }); } catch (error) { // If an error occurs after the file has been uploaded (e.g., DB error), // we must clean up the orphaned file from the disk. await cleanupUploadedFile(req.file); req.log.error({ error }, 'Error updating brand logo'); next(error); } }, ); /** * @openapi * /admin/unmatched-items: * get: * tags: [Admin] * summary: Get unmatched flyer items * description: Retrieve flyer items that couldn't be matched to master items. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: List of unmatched flyer items * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get( '/unmatched-items', validateRequest(emptySchema), async (req, res, next: NextFunction) => { try { const items = await db.adminRepo.getUnmatchedFlyerItems(req.log); sendSuccess(res, items); } catch (error) { req.log.error({ error }, 'Error fetching unmatched items'); next(error); } }, ); /** * @openapi * /admin/recipes/{recipeId}: * delete: * tags: [Admin] * summary: Delete a recipe * description: Admin endpoint to delete any recipe. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: recipeId * required: true * schema: * type: integer * description: Recipe ID * responses: * 204: * description: Recipe deleted successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Recipe not found */ router.delete( '/recipes/:recipeId', validateRequest(numericIdParam('recipeId')), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; // Infer the type directly from the schema generator function. // This was a duplicate, fixed. const { params } = req as unknown as z.infer>; try { // The isAdmin flag bypasses the ownership check in the repository method. await db.recipeRepo.deleteRecipe(params.recipeId, userProfile.user.user_id, true, req.log); sendNoContent(res); } catch (error: unknown) { req.log.error({ error }, 'Error deleting recipe'); next(error); } }, ); /** * @openapi * /admin/flyers/{flyerId}: * delete: * tags: [Admin] * summary: Delete a flyer * description: Admin endpoint to delete a flyer and its items. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: flyerId * required: true * schema: * type: integer * description: Flyer ID * responses: * 204: * description: Flyer deleted successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Flyer not found */ router.delete( '/flyers/:flyerId', validateRequest(numericIdParam('flyerId')), async (req: Request, res: Response, next: NextFunction) => { // Infer the type directly from the schema generator function. const { params } = req as unknown as z.infer>; try { await db.flyerRepo.deleteFlyer(params.flyerId, req.log); sendNoContent(res); } catch (error: unknown) { req.log.error({ error }, 'Error deleting flyer'); next(error); } }, ); /** * @openapi * /admin/comments/{id}/status: * put: * tags: [Admin] * summary: Update comment status * description: Update a recipe comment's visibility status. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Comment ID * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - status * properties: * status: * type: string * enum: [visible, hidden, reported] * responses: * 200: * description: Comment status updated successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Comment not found */ router.put( '/comments/:id/status', validateRequest(updateCommentStatusSchema), async (req: Request, res: Response, next: NextFunction) => { // Apply ADR-003 pattern for type safety const { params, body } = req as unknown as z.infer; try { const updatedComment = await db.adminRepo.updateRecipeCommentStatus( params.id, body.status, req.log, ); // This is still a standalone function in admin.db.ts sendSuccess(res, updatedComment); } catch (error: unknown) { req.log.error({ error }, 'Error updating comment status'); next(error); } }, ); /** * @openapi * /admin/users: * get: * tags: [Admin] * summary: Get all users * description: Retrieve a list of all users. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: List of all users * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get('/users', validateRequest(emptySchema), async (req, res, next: NextFunction) => { try { const users = await db.adminRepo.getAllUsers(req.log); sendSuccess(res, users); } catch (error) { req.log.error({ error }, 'Error fetching users'); next(error); } }); /** * @openapi * /admin/activity-log: * get: * tags: [Admin] * summary: Get activity log * description: Retrieve system activity log with pagination. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: query * name: limit * schema: * type: integer * default: 50 * description: Maximum number of entries to return * - in: query * name: offset * schema: * type: integer * default: 0 * description: Number of entries to skip * responses: * 200: * description: Activity log entries * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get( '/activity-log', validateRequest(activityLogSchema), async (req: Request, res: Response, next: NextFunction) => { // Apply ADR-003 pattern for type safety. // We parse the query here to apply Zod's coercions (string to number) and defaults. const { limit, offset } = activityLogSchema.shape.query.parse(req.query); try { const logs = await db.adminRepo.getActivityLog(limit!, offset!, req.log); sendSuccess(res, logs); } catch (error) { req.log.error({ error }, 'Error fetching activity log'); next(error); } }, ); /** * @openapi * /admin/users/{id}: * get: * tags: [Admin] * summary: Get user by ID * description: Retrieve a specific user's profile. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * format: uuid * description: User ID * responses: * 200: * description: User profile * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: User not found */ router.get( '/users/:id', validateRequest(uuidParamSchema('id', 'A valid user ID is required.')), async (req: Request, res: Response, next: NextFunction) => { // Apply ADR-003 pattern for type safety const { params } = req as unknown as z.infer>; try { const user = await db.userRepo.findUserProfileById(params.id, req.log); sendSuccess(res, user); } catch (error) { req.log.error({ error }, 'Error fetching user profile'); next(error); } }, ); /** * @openapi * /admin/users/{id}: * put: * tags: [Admin] * summary: Update user role * description: Update a user's role. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * format: uuid * description: User ID * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - role * properties: * role: * type: string * enum: [user, admin] * responses: * 200: * description: User role updated successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: User not found */ router.put( '/users/:id', validateRequest(updateUserRoleSchema), async (req: Request, res: Response, next: NextFunction) => { // Apply ADR-003 pattern for type safety const { params, body } = req as unknown as z.infer; try { const updatedUser = await db.adminRepo.updateUserRole(params.id, body.role, req.log); sendSuccess(res, updatedUser); } catch (error) { req.log.error({ error }, `Error updating user ${params.id}:`); next(error); } }, ); /** * @openapi * /admin/users/{id}: * delete: * tags: [Admin] * summary: Delete a user * description: Delete a user account. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * format: uuid * description: User ID * responses: * 204: * description: User deleted successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: User not found */ router.delete( '/users/:id', validateRequest(uuidParamSchema('id', 'A valid user ID is required.')), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; // Apply ADR-003 pattern for type safety const { params } = req as unknown as z.infer>; try { await userService.deleteUserAsAdmin(userProfile.user.user_id, params.id, req.log); sendNoContent(res); } catch (error) { req.log.error({ error }, 'Error deleting user'); next(error); } }, ); /** * @openapi * /admin/trigger/daily-deal-check: * post: * tags: [Admin] * summary: Trigger daily deal check * description: Manually trigger the daily deal check job. Requires admin role. * security: * - bearerAuth: [] * responses: * 202: * description: Job triggered successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.post( '/trigger/daily-deal-check', adminTriggerLimiter, validateRequest(emptySchema), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; req.log.info( `[Admin] Manual trigger for daily deal check received from user: ${userProfile.user.user_id}`, ); try { // We call the function but don't wait for it to finish (no `await`). // This is a "fire-and-forget" operation from the client's perspective. backgroundJobService.runDailyDealCheck(); sendSuccess( res, { message: 'Daily deal check job has been triggered successfully. It will run in the background.', }, 202, ); } catch (error) { req.log.error({ error }, '[Admin] Failed to trigger daily deal check job.'); next(error); } }, ); /** * @openapi * /admin/trigger/analytics-report: * post: * tags: [Admin] * summary: Trigger analytics report * description: Manually enqueue a job to generate the daily analytics report. Requires admin role. * security: * - bearerAuth: [] * responses: * 202: * description: Job enqueued successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.post( '/trigger/analytics-report', adminTriggerLimiter, validateRequest(emptySchema), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; req.log.info( `[Admin] Manual trigger for analytics report generation received from user: ${userProfile.user.user_id}`, ); try { const jobId = await backgroundJobService.triggerAnalyticsReport(); sendSuccess( res, { message: `Analytics report generation job has been enqueued successfully. Job ID: ${jobId}`, }, 202, ); } catch (error) { req.log.error({ error }, '[Admin] Failed to enqueue analytics report job.'); next(error); } }, ); /** * @openapi * /admin/flyers/{flyerId}/cleanup: * post: * tags: [Admin] * summary: Trigger flyer file cleanup * description: Enqueue a job to clean up a flyer's files. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: flyerId * required: true * schema: * type: integer * description: Flyer ID * responses: * 202: * description: Cleanup job enqueued successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Flyer not found */ router.post( '/flyers/:flyerId/cleanup', adminTriggerLimiter, validateRequest(numericIdParam('flyerId')), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; // Infer type from the schema generator for type safety, as per ADR-003. const { params } = req as unknown as z.infer>; // This was a duplicate, fixed. req.log.info( `[Admin] Manual trigger for flyer file cleanup received from user: ${userProfile.user.user_id} for flyer ID: ${params.flyerId}`, ); // Enqueue the cleanup job. The worker will handle the file deletion. try { await cleanupQueue.add('cleanup-flyer-files', { flyerId: params.flyerId }); sendSuccess( res, { message: `File cleanup job for flyer ID ${params.flyerId} has been enqueued.` }, 202, ); } catch (error) { req.log.error({ error }, 'Error enqueuing cleanup job'); next(error); } }, ); /** * @openapi * /admin/trigger/failing-job: * post: * tags: [Admin] * summary: Trigger failing test job * description: Enqueue a test job designed to fail for testing retry mechanisms. Requires admin role. * security: * - bearerAuth: [] * responses: * 202: * description: Failing test job enqueued successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.post( '/trigger/failing-job', adminTriggerLimiter, validateRequest(emptySchema), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; req.log.info( `[Admin] Manual trigger for a failing job received from user: ${userProfile.user.user_id}`, ); try { // Add a job with a special 'forceFail' flag that the worker will recognize. const job = await analyticsQueue.add('generate-daily-report', { reportDate: 'FAIL' }); sendSuccess( res, { message: `Failing test job has been enqueued successfully. Job ID: ${job.id}` }, 202, ); } catch (error) { req.log.error({ error }, 'Error enqueuing failing job'); next(error); } }, ); /** * @openapi * /admin/system/clear-geocode-cache: * post: * tags: [Admin] * summary: Clear geocode cache * description: Clears the Redis cache for geocoded addresses. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: Cache cleared successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.post( '/system/clear-geocode-cache', adminTriggerLimiter, validateRequest(emptySchema), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; req.log.info( `[Admin] Manual trigger for geocode cache clear received from user: ${userProfile.user.user_id}`, ); try { const keysDeleted = await geocodingService.clearGeocodeCache(req.log); sendSuccess(res, { message: `Successfully cleared the geocode cache. ${keysDeleted} keys were removed.`, }); } catch (error) { req.log.error({ error }, '[Admin] Failed to clear geocode cache.'); next(error); } }, ); /** * @openapi * /admin/workers/status: * get: * tags: [Admin] * summary: Get worker statuses * description: Get the current running status of all BullMQ workers. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: Worker status information * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get( '/workers/status', validateRequest(emptySchema), async (req: Request, res: Response, next: NextFunction) => { try { const workerStatuses = await monitoringService.getWorkerStatuses(); sendSuccess(res, workerStatuses); } catch (error) { req.log.error({ error }, 'Error fetching worker statuses'); next(error); } }, ); /** * @openapi * /admin/queues/status: * get: * tags: [Admin] * summary: Get queue statuses * description: Get job counts for all BullMQ queues. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: Queue status information * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.get( '/queues/status', validateRequest(emptySchema), async (req: Request, res: Response, next: NextFunction) => { try { const queueStatuses = await monitoringService.getQueueStatuses(); sendSuccess(res, queueStatuses); } catch (error) { req.log.error({ error }, 'Error fetching queue statuses'); next(error); } }, ); /** * @openapi * /admin/jobs/{queueName}/{jobId}/retry: * post: * tags: [Admin] * summary: Retry a failed job * description: Retries a specific failed job in a queue. Requires admin role. * security: * - bearerAuth: [] * parameters: * - in: path * name: queueName * required: true * schema: * type: string * enum: [flyer-processing, email-sending, analytics-reporting, file-cleanup, weekly-analytics-reporting] * description: Queue name * - in: path * name: jobId * required: true * schema: * type: string * description: Job ID * responses: * 200: * description: Job marked for retry successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required * 404: * description: Job not found */ router.post( '/jobs/:queueName/:jobId/retry', adminTriggerLimiter, validateRequest(jobRetrySchema), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; const { params: { queueName, jobId }, } = req as unknown as z.infer; try { await monitoringService.retryFailedJob(queueName, jobId, userProfile.user.user_id); sendSuccess(res, { message: `Job ${jobId} has been successfully marked for retry.` }); } catch (error) { req.log.error({ error }, 'Error retrying job'); next(error); } }, ); /** * @openapi * /admin/trigger/weekly-analytics: * post: * tags: [Admin] * summary: Trigger weekly analytics * description: Manually trigger the weekly analytics report job. Requires admin role. * security: * - bearerAuth: [] * responses: * 202: * description: Job enqueued successfully * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.post( '/trigger/weekly-analytics', adminTriggerLimiter, validateRequest(emptySchema), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; // This was a duplicate, fixed. req.log.info( `[Admin] Manual trigger for weekly analytics report received from user: ${userProfile.user.user_id}`, ); try { const jobId = await backgroundJobService.triggerWeeklyAnalyticsReport(); sendSuccess(res, { message: 'Successfully enqueued weekly analytics job.', jobId }, 202); } catch (error) { req.log.error({ error }, 'Error enqueuing weekly analytics job'); next(error); } }, ); /** * @openapi * /admin/system/clear-cache: * post: * tags: [Admin] * summary: Clear application cache * description: Clears cached flyers, brands, and stats data from Redis. Requires admin role. * security: * - bearerAuth: [] * responses: * 200: * description: Cache cleared successfully with details * 401: * description: Unauthorized * 403: * description: Forbidden - admin role required */ router.post( '/system/clear-cache', adminTriggerLimiter, validateRequest(emptySchema), async (req: Request, res: Response, next: NextFunction) => { const userProfile = req.user as UserProfile; req.log.info(`[Admin] Manual cache clear received from user: ${userProfile.user.user_id}`); try { const [flyersDeleted, brandsDeleted, statsDeleted] = await Promise.all([ cacheService.invalidateFlyers(req.log), cacheService.invalidateBrands(req.log), cacheService.invalidateStats(req.log), ]); const totalDeleted = flyersDeleted + brandsDeleted + statsDeleted; sendSuccess(res, { message: `Successfully cleared the application cache. ${totalDeleted} keys were removed.`, details: { flyers: flyersDeleted, brands: brandsDeleted, stats: statsDeleted, }, }); } catch (error) { req.log.error({ error }, '[Admin] Failed to clear application cache.'); next(error); } }, ); /* Catches errors from multer (e.g., file size, file filter) */ router.use(handleMulterError); export default router;