Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
1420 lines
45 KiB
TypeScript
1420 lines
45 KiB
TypeScript
// src/controllers/admin.controller.ts
|
|
// ============================================================================
|
|
// ADMIN CONTROLLER
|
|
// ============================================================================
|
|
// Provides administrative endpoints for managing the Flyer Crawler application.
|
|
// All endpoints require admin role authentication via @Security('bearerAuth', ['admin']).
|
|
//
|
|
// Endpoint Categories:
|
|
// - Corrections Management: Review, approve, reject, update suggested corrections
|
|
// - User Management: List, view, update role, delete users
|
|
// - Content Management: Review flyers, manage recipes, manage comments
|
|
// - Brand Management: Update brand logos (file upload)
|
|
// - Queue/Worker Monitoring: View queue and worker status, retry failed jobs
|
|
// - System Operations: Clear caches, trigger background jobs
|
|
// - Feature Flags: View current feature flag states
|
|
// - WebSocket Stats: View real-time connection statistics
|
|
//
|
|
// Note: Bull Board UI is mounted separately in server.ts due to its special
|
|
// routing requirements. This controller handles the JSON API endpoints only.
|
|
//
|
|
// Implements ADR-028 (API Response Format) via BaseController.
|
|
// ============================================================================
|
|
|
|
import {
|
|
Get,
|
|
Put,
|
|
Post,
|
|
Delete,
|
|
Route,
|
|
Tags,
|
|
Security,
|
|
Path,
|
|
Query,
|
|
Body,
|
|
Request,
|
|
SuccessResponse,
|
|
Response,
|
|
Middlewares,
|
|
} from 'tsoa';
|
|
import type { Request as ExpressRequest } from 'express';
|
|
import { BaseController } from './base.controller';
|
|
import type { SuccessResponse as SuccessResponseType, ErrorResponse } from './types';
|
|
import * as db from '../services/db/index.db';
|
|
import { backgroundJobService } from '../services/backgroundJobService';
|
|
import { monitoringService } from '../services/monitoringService.server';
|
|
import { geocodingService } from '../services/geocodingService.server';
|
|
import { cacheService } from '../services/cacheService.server';
|
|
import { brandService } from '../services/brandService';
|
|
import { userService } from '../services/userService';
|
|
import { getFeatureFlags } from '../services/featureFlags.server';
|
|
import { cleanupQueue, analyticsQueue } from '../services/queueService.server';
|
|
import type { UserProfile } from '../types';
|
|
import type { MessageResponse } from '../dtos/common.dto';
|
|
import { adminTriggerLimiter, adminUploadLimiter } from '../config/rateLimiters';
|
|
import { ValidationError } from '../services/db/errors.db';
|
|
import { cleanupUploadedFile } from '../utils/fileUtils';
|
|
|
|
// ============================================================================
|
|
// DTO TYPES FOR OPENAPI
|
|
// ============================================================================
|
|
// These Data Transfer Object types are tsoa-compatible versions of the
|
|
// domain types. They provide clear API contracts for OpenAPI generation.
|
|
// ============================================================================
|
|
|
|
// --- Corrections DTOs ---
|
|
|
|
/**
|
|
* Suggested correction data returned by admin endpoints.
|
|
*/
|
|
interface SuggestedCorrectionDto {
|
|
/** Unique identifier for the correction */
|
|
readonly suggested_correction_id: number;
|
|
/** ID of the flyer item being corrected */
|
|
readonly flyer_item_id: number;
|
|
/** User who submitted the correction */
|
|
readonly user_id: string;
|
|
/** Type of correction (e.g., 'master_item', 'category') */
|
|
correction_type: string;
|
|
/** The suggested new value */
|
|
suggested_value: string;
|
|
/** Current status of the correction */
|
|
status: 'pending' | 'approved' | 'rejected';
|
|
/** When the correction was last updated */
|
|
readonly updated_at: string;
|
|
/** When the correction was created */
|
|
readonly created_at: string;
|
|
/** Name of the flyer item (joined data) */
|
|
flyer_item_name?: string;
|
|
/** Price display of the flyer item (joined data) */
|
|
flyer_item_price_display?: string;
|
|
/** Email of the user who submitted (joined data) */
|
|
user_email?: string;
|
|
}
|
|
|
|
/**
|
|
* Request body for updating a correction's suggested value.
|
|
*/
|
|
interface UpdateCorrectionRequest {
|
|
/**
|
|
* The new suggested value for the correction.
|
|
* @minLength 1
|
|
*/
|
|
suggested_value: string;
|
|
}
|
|
|
|
// --- User Management DTOs ---
|
|
|
|
/**
|
|
* User data as seen by admins.
|
|
*/
|
|
interface AdminUserDto {
|
|
/** User's unique identifier (UUID) */
|
|
readonly user_id: string;
|
|
/** User's email address */
|
|
email: string;
|
|
/** User's role */
|
|
role: 'admin' | 'user';
|
|
/** User's full name */
|
|
full_name: string | null;
|
|
/** URL to user's avatar */
|
|
avatar_url: string | null;
|
|
/** Account creation timestamp */
|
|
readonly created_at: string;
|
|
}
|
|
|
|
/**
|
|
* Response for listing all users.
|
|
*/
|
|
interface UsersListResponse {
|
|
/** Array of user data */
|
|
users: AdminUserDto[];
|
|
/** Total number of users in the system */
|
|
total: number;
|
|
}
|
|
|
|
/**
|
|
* Request body for updating a user's role.
|
|
*/
|
|
interface UpdateUserRoleRequest {
|
|
/** The new role to assign */
|
|
role: 'user' | 'admin';
|
|
}
|
|
|
|
/**
|
|
* Profile data returned when updating a user.
|
|
*/
|
|
interface ProfileDto {
|
|
/** User's full name */
|
|
full_name?: string | null;
|
|
/** URL to user's avatar */
|
|
avatar_url?: string | null;
|
|
/** Associated address ID */
|
|
address_id?: number | null;
|
|
/** User's points balance */
|
|
readonly points: number;
|
|
/** User's role */
|
|
readonly role: 'admin' | 'user';
|
|
/** User preferences */
|
|
preferences?: {
|
|
darkMode?: boolean;
|
|
unitSystem?: 'metric' | 'imperial';
|
|
} | null;
|
|
/** Timestamp of creation */
|
|
readonly created_at: string;
|
|
/** Timestamp of last update */
|
|
readonly updated_at: string;
|
|
}
|
|
|
|
// --- Recipe DTOs ---
|
|
|
|
/**
|
|
* Recipe data returned by admin endpoints.
|
|
* Named AdminRecipeDto to avoid conflict with recipe.controller.ts RecipeDto.
|
|
*/
|
|
interface AdminRecipeDto {
|
|
/** Unique recipe identifier */
|
|
readonly recipe_id: number;
|
|
/** Owner's user ID */
|
|
readonly user_id?: string | null;
|
|
/** Recipe name */
|
|
name: string;
|
|
/** Recipe description */
|
|
description?: string | null;
|
|
/** Cooking instructions */
|
|
instructions?: string | null;
|
|
/** Preparation time in minutes */
|
|
prep_time_minutes?: number | null;
|
|
/** Cooking time in minutes */
|
|
cook_time_minutes?: number | null;
|
|
/** Number of servings */
|
|
servings?: number | null;
|
|
/** URL to recipe photo */
|
|
photo_url?: string | null;
|
|
/** Average rating */
|
|
readonly avg_rating: number;
|
|
/** Publication status */
|
|
status: 'private' | 'pending_review' | 'public' | 'rejected';
|
|
/** Number of ratings */
|
|
readonly rating_count: number;
|
|
/** Timestamp of creation */
|
|
readonly created_at: string;
|
|
/** Timestamp of last update */
|
|
readonly updated_at: string;
|
|
}
|
|
|
|
/**
|
|
* Request body for updating recipe status.
|
|
*/
|
|
interface UpdateRecipeStatusRequest {
|
|
/** The new status for the recipe */
|
|
status: 'private' | 'pending_review' | 'public' | 'rejected';
|
|
}
|
|
|
|
// --- Comment DTOs ---
|
|
|
|
/**
|
|
* Recipe comment data returned by admin endpoints.
|
|
* Named AdminRecipeCommentDto to avoid conflict with recipe.controller.ts.
|
|
*/
|
|
interface AdminRecipeCommentDto {
|
|
/** Unique comment identifier */
|
|
readonly recipe_comment_id: number;
|
|
/** Parent recipe ID */
|
|
readonly recipe_id: number;
|
|
/** Author's user ID */
|
|
readonly user_id: string;
|
|
/** Parent comment ID for replies */
|
|
readonly parent_comment_id?: number | null;
|
|
/** Comment content */
|
|
content: string;
|
|
/** Visibility status */
|
|
status: 'visible' | 'hidden' | 'reported';
|
|
/** Timestamp of creation */
|
|
readonly created_at: string;
|
|
/** Timestamp of last update */
|
|
readonly updated_at: string;
|
|
}
|
|
|
|
/**
|
|
* Request body for updating comment status.
|
|
*/
|
|
interface UpdateCommentStatusRequest {
|
|
/** The new status for the comment */
|
|
status: 'visible' | 'hidden' | 'reported';
|
|
}
|
|
|
|
// --- Flyer DTOs ---
|
|
|
|
/**
|
|
* Flyer data returned by admin review endpoints.
|
|
*/
|
|
interface AdminFlyerDto {
|
|
/** Unique flyer identifier */
|
|
readonly flyer_id: number;
|
|
/** Original filename */
|
|
file_name: string;
|
|
/** URL to flyer image */
|
|
image_url: string;
|
|
/** URL to 64x64 icon */
|
|
icon_url: string;
|
|
/** Processing status */
|
|
status: 'processed' | 'needs_review' | 'archived';
|
|
/** Number of items in the flyer */
|
|
item_count: number;
|
|
/** Start date for deals */
|
|
valid_from?: string | null;
|
|
/** End date for deals */
|
|
valid_to?: string | null;
|
|
/** Timestamp of creation */
|
|
readonly created_at: string;
|
|
/** Timestamp of last update */
|
|
readonly updated_at: string;
|
|
}
|
|
|
|
// --- Unmatched Items DTOs ---
|
|
|
|
/**
|
|
* Unmatched flyer item data for admin review.
|
|
*/
|
|
interface UnmatchedFlyerItemDto {
|
|
/** Unique identifier */
|
|
readonly unmatched_flyer_item_id: number;
|
|
/** Current status */
|
|
status: 'pending' | 'resolved' | 'ignored';
|
|
/** ID of the flyer item */
|
|
readonly flyer_item_id: number;
|
|
/** Item name from the flyer */
|
|
flyer_item_name: string;
|
|
/** Price display text */
|
|
price_display: string;
|
|
/** Parent flyer ID */
|
|
flyer_id: number;
|
|
/** Store name */
|
|
store_name: string;
|
|
/** Timestamp of creation */
|
|
readonly created_at: string;
|
|
/** Timestamp of last update */
|
|
readonly updated_at: string;
|
|
}
|
|
|
|
// --- Brand DTOs ---
|
|
|
|
/**
|
|
* Brand data returned by admin endpoints.
|
|
*/
|
|
interface BrandDto {
|
|
/** Unique brand identifier */
|
|
readonly brand_id: number;
|
|
/** Brand name */
|
|
name: string;
|
|
/** URL to brand logo */
|
|
logo_url?: string | null;
|
|
/** Associated store ID */
|
|
readonly store_id?: number | null;
|
|
/** Timestamp of creation */
|
|
readonly created_at: string;
|
|
/** Timestamp of last update */
|
|
readonly updated_at: string;
|
|
}
|
|
|
|
/**
|
|
* Response after uploading a brand logo.
|
|
*/
|
|
interface BrandLogoResponse {
|
|
/** Success message */
|
|
message: string;
|
|
/** URL to the uploaded logo */
|
|
logoUrl: string;
|
|
}
|
|
|
|
// --- Stats DTOs ---
|
|
|
|
/**
|
|
* Application-wide statistics.
|
|
*/
|
|
interface ApplicationStatsDto {
|
|
/** Total number of flyers */
|
|
flyerCount: number;
|
|
/** Total number of users */
|
|
userCount: number;
|
|
/** Total number of flyer items */
|
|
flyerItemCount: number;
|
|
/** Total number of stores */
|
|
storeCount: number;
|
|
/** Number of pending corrections */
|
|
pendingCorrectionCount: number;
|
|
/** Total number of recipes */
|
|
recipeCount: number;
|
|
}
|
|
|
|
/**
|
|
* Daily statistics for the dashboard.
|
|
*/
|
|
interface DailyStatsDto {
|
|
/** Date in YYYY-MM-DD format */
|
|
date: string;
|
|
/** Number of new users registered */
|
|
new_users: number;
|
|
/** Number of new flyers uploaded */
|
|
new_flyers: number;
|
|
}
|
|
|
|
// --- Activity Log DTOs ---
|
|
|
|
/**
|
|
* Activity log entry.
|
|
*/
|
|
interface ActivityLogItemDto {
|
|
/** Unique log entry ID */
|
|
readonly activity_log_id: number;
|
|
/** User who performed the action (null for system actions) */
|
|
readonly user_id: string | null;
|
|
/** Type of action */
|
|
action: string;
|
|
/** Human-readable description */
|
|
display_text: string;
|
|
/** Icon identifier */
|
|
icon?: string | null;
|
|
/** Timestamp of the activity */
|
|
readonly created_at: string;
|
|
/** User's name (joined data) */
|
|
user_full_name?: string;
|
|
/** User's avatar URL (joined data) */
|
|
user_avatar_url?: string;
|
|
}
|
|
|
|
// --- Queue/Worker DTOs ---
|
|
|
|
/**
|
|
* Worker status information.
|
|
*/
|
|
interface WorkerStatusDto {
|
|
/** Worker name */
|
|
name: string;
|
|
/** Whether the worker is currently running */
|
|
isRunning: boolean;
|
|
}
|
|
|
|
/**
|
|
* Queue status with job counts.
|
|
*/
|
|
interface QueueStatusDto {
|
|
/** Queue name */
|
|
name: string;
|
|
/** Job counts by state */
|
|
counts: {
|
|
waiting: number;
|
|
active: number;
|
|
completed: number;
|
|
failed: number;
|
|
delayed: number;
|
|
paused: number;
|
|
};
|
|
}
|
|
|
|
// RetryJobRequest is not needed - queueName and jobId come from path parameters
|
|
|
|
// --- System DTOs ---
|
|
|
|
/**
|
|
* Response for cache clear operations.
|
|
*/
|
|
interface CacheClearResponse {
|
|
/** Success message */
|
|
message: string;
|
|
/** Details of what was cleared */
|
|
details?: {
|
|
flyers?: number;
|
|
brands?: number;
|
|
stats?: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Response for triggered background jobs.
|
|
*/
|
|
interface JobTriggeredResponse {
|
|
/** Success message */
|
|
message: string;
|
|
/** Job ID if available */
|
|
jobId?: string;
|
|
}
|
|
|
|
// --- Feature Flags DTOs ---
|
|
|
|
/**
|
|
* Feature flags response.
|
|
*/
|
|
interface FeatureFlagsResponse {
|
|
/** Map of feature flag names to their enabled state */
|
|
flags: Record<string, boolean>;
|
|
}
|
|
|
|
// --- WebSocket DTOs ---
|
|
|
|
/**
|
|
* WebSocket connection statistics.
|
|
*/
|
|
interface WebSocketStatsDto {
|
|
/** Number of unique users with active connections */
|
|
totalUsers: number;
|
|
/** Total number of active WebSocket connections */
|
|
totalConnections: number;
|
|
}
|
|
|
|
// --- User Profile DTOs ---
|
|
|
|
/**
|
|
* User profile data returned by admin user view endpoint.
|
|
* Named AdminViewUserProfileDto to avoid conflict with common.dto.ts UserProfileDto.
|
|
*/
|
|
interface AdminViewUserProfileDto {
|
|
/** User's full name */
|
|
full_name?: string | null;
|
|
/** URL to user's avatar */
|
|
avatar_url?: string | null;
|
|
/** Associated address ID */
|
|
address_id?: number | null;
|
|
/** User's points balance */
|
|
readonly points: number;
|
|
/** User's role */
|
|
readonly role: 'admin' | 'user';
|
|
/** Timestamp of creation */
|
|
readonly created_at: string;
|
|
/** Timestamp of last update */
|
|
readonly updated_at: string;
|
|
/** Nested user data */
|
|
user: {
|
|
user_id: string;
|
|
email: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
}
|
|
|
|
// Note: MessageResponse is imported from common.dto.ts below
|
|
|
|
// ============================================================================
|
|
// ADMIN CONTROLLER
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Administrative controller for managing the Flyer Crawler application.
|
|
*
|
|
* All endpoints in this controller require admin role authentication.
|
|
* The Bull Board UI for queue management is mounted separately at
|
|
* /api/v1/admin/jobs and is not part of this controller.
|
|
*
|
|
* Rate limiting is applied to sensitive operations:
|
|
* - adminTriggerLimiter: For triggering background jobs (30 req/15 min)
|
|
* - adminUploadLimiter: For file uploads (20 req/15 min)
|
|
*/
|
|
@Route('admin')
|
|
@Tags('Admin')
|
|
@Security('bearerAuth', ['admin'])
|
|
export class AdminController extends BaseController {
|
|
// ==========================================================================
|
|
// CORRECTIONS MANAGEMENT
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get all pending corrections.
|
|
*
|
|
* Retrieves all suggested corrections that are pending review.
|
|
* Includes context about the flyer item and the user who submitted.
|
|
*
|
|
* @summary Get pending corrections
|
|
* @param request Express request for logging
|
|
* @returns List of pending suggested corrections
|
|
*/
|
|
@Get('corrections')
|
|
@SuccessResponse(200, 'List of pending corrections')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getCorrections(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<SuggestedCorrectionDto[]>> {
|
|
const corrections = await db.adminRepo.getSuggestedCorrections(request.log);
|
|
return this.success(corrections as SuggestedCorrectionDto[]);
|
|
}
|
|
|
|
/**
|
|
* Approve a correction.
|
|
*
|
|
* Approves a pending correction and applies the change to the flyer item.
|
|
* This operation is transactional.
|
|
*
|
|
* @summary Approve a correction
|
|
* @param id Correction ID
|
|
* @param request Express request for logging
|
|
* @returns Success message
|
|
*/
|
|
@Post('corrections/{id}/approve')
|
|
@SuccessResponse(200, 'Correction approved')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'Correction not found')
|
|
public async approveCorrection(
|
|
@Path() id: number,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<MessageResponse>> {
|
|
await db.adminRepo.approveCorrection(id, request.log);
|
|
return this.success({ message: 'Correction approved successfully.' });
|
|
}
|
|
|
|
/**
|
|
* Reject a correction.
|
|
*
|
|
* Rejects a pending correction without applying any changes.
|
|
*
|
|
* @summary Reject a correction
|
|
* @param id Correction ID
|
|
* @param request Express request for logging
|
|
* @returns Success message
|
|
*/
|
|
@Post('corrections/{id}/reject')
|
|
@SuccessResponse(200, 'Correction rejected')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'Correction not found')
|
|
public async rejectCorrection(
|
|
@Path() id: number,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<MessageResponse>> {
|
|
await db.adminRepo.rejectCorrection(id, request.log);
|
|
return this.success({ message: 'Correction rejected successfully.' });
|
|
}
|
|
|
|
/**
|
|
* Update a correction's suggested value.
|
|
*
|
|
* Allows an admin to modify the suggested value before approving.
|
|
*
|
|
* @summary Update correction value
|
|
* @param id Correction ID
|
|
* @param body New suggested value
|
|
* @param request Express request for logging
|
|
* @returns The updated correction
|
|
*/
|
|
@Put('corrections/{id}')
|
|
@SuccessResponse(200, 'Correction updated')
|
|
@Response<ErrorResponse>(400, 'Invalid suggested value')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'Correction not found')
|
|
public async updateCorrection(
|
|
@Path() id: number,
|
|
@Body() body: UpdateCorrectionRequest,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<SuggestedCorrectionDto>> {
|
|
const updatedCorrection = await db.adminRepo.updateSuggestedCorrection(
|
|
id,
|
|
body.suggested_value,
|
|
request.log,
|
|
);
|
|
return this.success(updatedCorrection as SuggestedCorrectionDto);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// USER MANAGEMENT
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get all users.
|
|
*
|
|
* Retrieves a paginated list of all users with their profiles.
|
|
*
|
|
* @summary List all users
|
|
* @param request Express request for logging
|
|
* @param limit Maximum users to return (1-100)
|
|
* @param offset Number of users to skip
|
|
* @returns List of users with total count
|
|
*/
|
|
@Get('users')
|
|
@SuccessResponse(200, 'List of users')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getUsers(
|
|
@Request() request: ExpressRequest,
|
|
@Query() limit?: number,
|
|
@Query() offset?: number,
|
|
): Promise<SuccessResponseType<UsersListResponse>> {
|
|
// Normalize pagination
|
|
const normalizedLimit =
|
|
limit !== undefined ? Math.min(100, Math.max(1, Math.floor(limit))) : undefined;
|
|
const normalizedOffset = offset !== undefined ? Math.max(0, Math.floor(offset)) : 0;
|
|
|
|
const result = await db.adminRepo.getAllUsers(request.log, normalizedLimit, normalizedOffset);
|
|
return this.success({
|
|
users: result.users as AdminUserDto[],
|
|
total: result.total,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get user by ID.
|
|
*
|
|
* Retrieves a specific user's profile.
|
|
*
|
|
* @summary Get user profile
|
|
* @param id User ID (UUID)
|
|
* @param request Express request for logging
|
|
* @returns User profile data
|
|
*/
|
|
@Get('users/{id}')
|
|
@SuccessResponse(200, 'User profile')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'User not found')
|
|
public async getUserById(
|
|
@Path() id: string,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<AdminViewUserProfileDto>> {
|
|
const user = await db.userRepo.findUserProfileById(id, request.log);
|
|
return this.success(user as unknown as AdminViewUserProfileDto);
|
|
}
|
|
|
|
/**
|
|
* Update user role.
|
|
*
|
|
* Changes a user's role between 'user' and 'admin'.
|
|
*
|
|
* @summary Update user role
|
|
* @param id User ID (UUID)
|
|
* @param body New role
|
|
* @param request Express request for logging
|
|
* @returns Updated profile
|
|
*/
|
|
@Put('users/{id}')
|
|
@SuccessResponse(200, 'Role updated')
|
|
@Response<ErrorResponse>(400, 'Invalid role')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'User not found')
|
|
public async updateUserRole(
|
|
@Path() id: string,
|
|
@Body() body: UpdateUserRoleRequest,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<ProfileDto>> {
|
|
const updatedUser = await db.adminRepo.updateUserRole(id, body.role, request.log);
|
|
return this.success(updatedUser as ProfileDto);
|
|
}
|
|
|
|
/**
|
|
* Delete a user.
|
|
*
|
|
* Permanently deletes a user account. Admins cannot delete their own account.
|
|
*
|
|
* @summary Delete user
|
|
* @param id User ID (UUID)
|
|
* @param request Express request for logging
|
|
*/
|
|
@Delete('users/{id}')
|
|
@SuccessResponse(204, 'User deleted')
|
|
@Response<ErrorResponse>(400, 'Cannot delete own account')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'User not found')
|
|
public async deleteUser(@Path() id: string, @Request() request: ExpressRequest): Promise<void> {
|
|
const userProfile = request.user as UserProfile;
|
|
await userService.deleteUserAsAdmin(userProfile.user.user_id, id, request.log);
|
|
this.noContent();
|
|
}
|
|
|
|
// ==========================================================================
|
|
// CONTENT MANAGEMENT - RECIPES
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Update recipe status.
|
|
*
|
|
* Changes a recipe's publication status for moderation purposes.
|
|
*
|
|
* @summary Update recipe status
|
|
* @param id Recipe ID
|
|
* @param body New status
|
|
* @param request Express request for logging
|
|
* @returns Updated recipe
|
|
*/
|
|
@Put('recipes/{id}/status')
|
|
@SuccessResponse(200, 'Recipe status updated')
|
|
@Response<ErrorResponse>(400, 'Invalid status')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'Recipe not found')
|
|
public async updateRecipeStatus(
|
|
@Path() id: number,
|
|
@Body() body: UpdateRecipeStatusRequest,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<AdminRecipeDto>> {
|
|
const updatedRecipe = await db.adminRepo.updateRecipeStatus(id, body.status, request.log);
|
|
return this.success(updatedRecipe as AdminRecipeDto);
|
|
}
|
|
|
|
/**
|
|
* Delete a recipe.
|
|
*
|
|
* Permanently deletes any recipe regardless of ownership.
|
|
*
|
|
* @summary Delete recipe (Admin)
|
|
* @param recipeId Recipe ID
|
|
* @param request Express request for logging
|
|
*/
|
|
@Delete('recipes/{recipeId}')
|
|
@SuccessResponse(204, 'Recipe deleted')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'Recipe not found')
|
|
public async deleteRecipe(
|
|
@Path() recipeId: number,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<void> {
|
|
const userProfile = request.user as UserProfile;
|
|
// Pass isAdmin=true to bypass ownership check
|
|
await db.recipeRepo.deleteRecipe(recipeId, userProfile.user.user_id, true, request.log);
|
|
this.noContent();
|
|
}
|
|
|
|
// ==========================================================================
|
|
// CONTENT MANAGEMENT - COMMENTS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Update comment status.
|
|
*
|
|
* Changes a recipe comment's visibility status for moderation.
|
|
*
|
|
* @summary Update comment status
|
|
* @param id Comment ID
|
|
* @param body New status
|
|
* @param request Express request for logging
|
|
* @returns Updated comment
|
|
*/
|
|
@Put('comments/{id}/status')
|
|
@SuccessResponse(200, 'Comment status updated')
|
|
@Response<ErrorResponse>(400, 'Invalid status')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'Comment not found')
|
|
public async updateCommentStatus(
|
|
@Path() id: number,
|
|
@Body() body: UpdateCommentStatusRequest,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<AdminRecipeCommentDto>> {
|
|
const updatedComment = await db.adminRepo.updateRecipeCommentStatus(
|
|
id,
|
|
body.status,
|
|
request.log,
|
|
);
|
|
return this.success(updatedComment as AdminRecipeCommentDto);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// CONTENT MANAGEMENT - FLYERS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get flyers needing review.
|
|
*
|
|
* Retrieves all flyers with 'needs_review' status.
|
|
*
|
|
* @summary Get flyers for review
|
|
* @param request Express request for logging
|
|
* @returns List of flyers needing review
|
|
*/
|
|
@Get('review/flyers')
|
|
@SuccessResponse(200, 'List of flyers for review')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getFlyersForReview(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<AdminFlyerDto[]>> {
|
|
request.log.debug('Fetching flyers for review via adminRepo');
|
|
const flyers = await db.adminRepo.getFlyersForReview(request.log);
|
|
request.log.info(
|
|
{ count: Array.isArray(flyers) ? flyers.length : 'unknown' },
|
|
'Successfully fetched flyers for review',
|
|
);
|
|
return this.success(flyers as unknown as AdminFlyerDto[]);
|
|
}
|
|
|
|
/**
|
|
* Delete a flyer.
|
|
*
|
|
* Permanently deletes a flyer and all its items.
|
|
*
|
|
* @summary Delete flyer
|
|
* @param flyerId Flyer ID
|
|
* @param request Express request for logging
|
|
*/
|
|
@Delete('flyers/{flyerId}')
|
|
@SuccessResponse(204, 'Flyer deleted')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'Flyer not found')
|
|
public async deleteFlyer(
|
|
@Path() flyerId: number,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<void> {
|
|
await db.flyerRepo.deleteFlyer(flyerId, request.log);
|
|
this.noContent();
|
|
}
|
|
|
|
/**
|
|
* Trigger flyer file cleanup.
|
|
*
|
|
* Enqueues a background job to clean up files associated with a flyer.
|
|
*
|
|
* @summary Trigger flyer cleanup
|
|
* @param flyerId Flyer ID
|
|
* @param request Express request for logging
|
|
* @returns Job enqueued message
|
|
*/
|
|
@Post('flyers/{flyerId}/cleanup')
|
|
@Middlewares(adminTriggerLimiter)
|
|
@SuccessResponse(202, 'Cleanup job enqueued')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async triggerFlyerCleanup(
|
|
@Path() flyerId: number,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<JobTriggeredResponse>> {
|
|
const userProfile = request.user as UserProfile;
|
|
request.log.info(
|
|
`[Admin] Manual trigger for flyer file cleanup received from user: ${userProfile.user.user_id} for flyer ID: ${flyerId}`,
|
|
);
|
|
|
|
await cleanupQueue.add('cleanup-flyer-files', { flyerId });
|
|
this.setStatus(202);
|
|
return this.success({ message: `File cleanup job for flyer ID ${flyerId} has been enqueued.` });
|
|
}
|
|
|
|
// ==========================================================================
|
|
// CONTENT MANAGEMENT - UNMATCHED ITEMS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get unmatched flyer items.
|
|
*
|
|
* Retrieves flyer items that couldn't be matched to master items.
|
|
*
|
|
* @summary Get unmatched items
|
|
* @param request Express request for logging
|
|
* @returns List of unmatched items
|
|
*/
|
|
@Get('unmatched-items')
|
|
@SuccessResponse(200, 'List of unmatched items')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getUnmatchedItems(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<UnmatchedFlyerItemDto[]>> {
|
|
const items = await db.adminRepo.getUnmatchedFlyerItems(request.log);
|
|
return this.success(items as UnmatchedFlyerItemDto[]);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// BRAND MANAGEMENT
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get all brands.
|
|
*
|
|
* Retrieves a list of all brands in the system.
|
|
*
|
|
* @summary Get all brands
|
|
* @param request Express request for logging
|
|
* @returns List of brands
|
|
*/
|
|
@Get('brands')
|
|
@SuccessResponse(200, 'List of brands')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getBrands(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<BrandDto[]>> {
|
|
const brands = await db.flyerRepo.getAllBrands(request.log);
|
|
return this.success(brands as BrandDto[]);
|
|
}
|
|
|
|
/**
|
|
* Upload brand logo.
|
|
*
|
|
* Uploads or updates a brand's logo image.
|
|
* Accepts JPEG, PNG, GIF, or WebP images up to 2MB.
|
|
*
|
|
* Note: File upload handling requires multer middleware to be configured
|
|
* in the route registration. The file is accessed via request.file.
|
|
*
|
|
* @summary Upload brand logo
|
|
* @param id Brand ID
|
|
* @param request Express request with uploaded file
|
|
* @returns Success message with logo URL
|
|
*/
|
|
@Post('brands/{id}/logo')
|
|
@Middlewares(adminUploadLimiter)
|
|
@SuccessResponse(200, 'Logo uploaded')
|
|
@Response<ErrorResponse>(400, 'Invalid file or missing logo')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'Brand not found')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async uploadBrandLogo(
|
|
@Path() id: number,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<BrandLogoResponse>> {
|
|
// Get file from request (uploaded by multer middleware)
|
|
const logoImage = request.file as Express.Multer.File | undefined;
|
|
|
|
try {
|
|
// Validate file was uploaded
|
|
if (!logoImage) {
|
|
throw new ValidationError([], 'Logo image file is missing.');
|
|
}
|
|
|
|
const logoUrl = await brandService.updateBrandLogo(id, logoImage, request.log);
|
|
|
|
request.log.info({ brandId: id, logoUrl }, `Brand logo updated for brand ID: ${id}`);
|
|
return this.success({ message: 'Brand logo updated successfully.', logoUrl });
|
|
} catch (error) {
|
|
// Clean up the uploaded file if an error occurred
|
|
await cleanupUploadedFile(logoImage);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// STATISTICS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get application statistics.
|
|
*
|
|
* Retrieves overall application statistics for the admin dashboard.
|
|
*
|
|
* @summary Get application stats
|
|
* @param request Express request for logging
|
|
* @returns Application statistics
|
|
*/
|
|
@Get('stats')
|
|
@SuccessResponse(200, 'Application statistics')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getStats(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<ApplicationStatsDto>> {
|
|
const stats = await db.adminRepo.getApplicationStats(request.log);
|
|
return this.success(stats);
|
|
}
|
|
|
|
/**
|
|
* Get daily statistics.
|
|
*
|
|
* Retrieves daily user registration and flyer upload statistics
|
|
* for the last 30 days.
|
|
*
|
|
* @summary Get daily stats
|
|
* @param request Express request for logging
|
|
* @returns Daily statistics array
|
|
*/
|
|
@Get('stats/daily')
|
|
@SuccessResponse(200, 'Daily statistics')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getDailyStats(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<DailyStatsDto[]>> {
|
|
const dailyStats = await db.adminRepo.getDailyStatsForLast30Days(request.log);
|
|
return this.success(dailyStats);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// ACTIVITY LOG
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get activity log.
|
|
*
|
|
* Retrieves recent system activity with pagination.
|
|
*
|
|
* @summary Get activity log
|
|
* @param request Express request for logging
|
|
* @param limit Maximum entries to return (default: 50)
|
|
* @param offset Number of entries to skip (default: 0)
|
|
* @returns Activity log entries
|
|
*/
|
|
@Get('activity-log')
|
|
@SuccessResponse(200, 'Activity log entries')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getActivityLog(
|
|
@Request() request: ExpressRequest,
|
|
@Query() limit?: number,
|
|
@Query() offset?: number,
|
|
): Promise<SuccessResponseType<ActivityLogItemDto[]>> {
|
|
const normalizedLimit = limit ?? 50;
|
|
const normalizedOffset = offset ?? 0;
|
|
|
|
const logs = await db.adminRepo.getActivityLog(normalizedLimit, normalizedOffset, request.log);
|
|
return this.success(logs as unknown as ActivityLogItemDto[]);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// QUEUE AND WORKER MONITORING
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get worker statuses.
|
|
*
|
|
* Returns the running status of all BullMQ workers.
|
|
*
|
|
* @summary Get worker statuses
|
|
* @returns Array of worker statuses
|
|
*/
|
|
@Get('workers/status')
|
|
@SuccessResponse(200, 'Worker statuses')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getWorkerStatuses(
|
|
@Request() _request: ExpressRequest,
|
|
): Promise<SuccessResponseType<WorkerStatusDto[]>> {
|
|
const workerStatuses = await monitoringService.getWorkerStatuses();
|
|
return this.success(workerStatuses);
|
|
}
|
|
|
|
/**
|
|
* Get queue statuses.
|
|
*
|
|
* Returns job counts for all BullMQ queues.
|
|
*
|
|
* @summary Get queue statuses
|
|
* @returns Array of queue statuses
|
|
*/
|
|
@Get('queues/status')
|
|
@SuccessResponse(200, 'Queue statuses')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getQueueStatuses(
|
|
@Request() _request: ExpressRequest,
|
|
): Promise<SuccessResponseType<QueueStatusDto[]>> {
|
|
const queueStatuses = await monitoringService.getQueueStatuses();
|
|
return this.success(queueStatuses);
|
|
}
|
|
|
|
/**
|
|
* Retry a failed job.
|
|
*
|
|
* Marks a failed job in a queue for retry.
|
|
*
|
|
* @summary Retry failed job
|
|
* @param queueName Queue name
|
|
* @param jobId Job ID
|
|
* @param request Express request for logging
|
|
* @returns Success message
|
|
*/
|
|
@Post('jobs/{queueName}/{jobId}/retry')
|
|
@Middlewares(adminTriggerLimiter)
|
|
@SuccessResponse(200, 'Job marked for retry')
|
|
@Response<ErrorResponse>(400, 'Job is not in failed state')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(404, 'Queue or job not found')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async retryJob(
|
|
@Path()
|
|
queueName:
|
|
| 'flyer-processing'
|
|
| 'email-sending'
|
|
| 'analytics-reporting'
|
|
| 'file-cleanup'
|
|
| 'weekly-analytics-reporting'
|
|
| 'token-cleanup',
|
|
@Path() jobId: string,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<MessageResponse>> {
|
|
const userProfile = request.user as UserProfile;
|
|
await monitoringService.retryFailedJob(queueName, jobId, userProfile.user.user_id);
|
|
return this.success({ message: `Job ${jobId} has been successfully marked for retry.` });
|
|
}
|
|
|
|
// ==========================================================================
|
|
// BACKGROUND JOB TRIGGERS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Trigger daily deal check.
|
|
*
|
|
* Manually triggers the daily deal check background job.
|
|
* This is a fire-and-forget operation.
|
|
*
|
|
* @summary Trigger daily deal check
|
|
* @param request Express request for logging
|
|
* @returns Job triggered message
|
|
*/
|
|
@Post('trigger/daily-deal-check')
|
|
@Middlewares(adminTriggerLimiter)
|
|
@SuccessResponse(202, 'Job triggered')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async triggerDailyDealCheck(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<JobTriggeredResponse>> {
|
|
const userProfile = request.user as UserProfile;
|
|
request.log.info(
|
|
`[Admin] Manual trigger for daily deal check received from user: ${userProfile.user.user_id}`,
|
|
);
|
|
|
|
// Fire-and-forget operation
|
|
backgroundJobService.runDailyDealCheck();
|
|
this.setStatus(202);
|
|
return this.success({
|
|
message:
|
|
'Daily deal check job has been triggered successfully. It will run in the background.',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Trigger analytics report.
|
|
*
|
|
* Enqueues a job to generate the daily analytics report.
|
|
*
|
|
* @summary Trigger analytics report
|
|
* @param request Express request for logging
|
|
* @returns Job enqueued message with job ID
|
|
*/
|
|
@Post('trigger/analytics-report')
|
|
@Middlewares(adminTriggerLimiter)
|
|
@SuccessResponse(202, 'Job enqueued')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async triggerAnalyticsReport(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<JobTriggeredResponse>> {
|
|
const userProfile = request.user as UserProfile;
|
|
request.log.info(
|
|
`[Admin] Manual trigger for analytics report generation received from user: ${userProfile.user.user_id}`,
|
|
);
|
|
|
|
const jobId = await backgroundJobService.triggerAnalyticsReport();
|
|
this.setStatus(202);
|
|
return this.success({
|
|
message: `Analytics report generation job has been enqueued successfully. Job ID: ${jobId}`,
|
|
jobId,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Trigger weekly analytics.
|
|
*
|
|
* Enqueues a job to generate the weekly analytics report.
|
|
*
|
|
* @summary Trigger weekly analytics
|
|
* @param request Express request for logging
|
|
* @returns Job enqueued message with job ID
|
|
*/
|
|
@Post('trigger/weekly-analytics')
|
|
@Middlewares(adminTriggerLimiter)
|
|
@SuccessResponse(202, 'Job enqueued')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async triggerWeeklyAnalytics(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<JobTriggeredResponse>> {
|
|
const userProfile = request.user as UserProfile;
|
|
request.log.info(
|
|
`[Admin] Manual trigger for weekly analytics report received from user: ${userProfile.user.user_id}`,
|
|
);
|
|
|
|
const jobId = await backgroundJobService.triggerWeeklyAnalyticsReport();
|
|
this.setStatus(202);
|
|
return this.success({ message: 'Successfully enqueued weekly analytics job.', jobId });
|
|
}
|
|
|
|
/**
|
|
* Trigger token cleanup.
|
|
*
|
|
* Enqueues a job to clean up expired password reset tokens.
|
|
*
|
|
* @summary Trigger token cleanup
|
|
* @param request Express request for logging
|
|
* @returns Job enqueued message with job ID
|
|
*/
|
|
@Post('trigger/token-cleanup')
|
|
@Middlewares(adminTriggerLimiter)
|
|
@SuccessResponse(202, 'Job enqueued')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async triggerTokenCleanup(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<JobTriggeredResponse>> {
|
|
const userProfile = request.user as UserProfile;
|
|
request.log.info(
|
|
`[Admin] Manual trigger for token cleanup received from user: ${userProfile.user.user_id}`,
|
|
);
|
|
|
|
const jobId = await backgroundJobService.triggerTokenCleanup();
|
|
this.setStatus(202);
|
|
return this.success({ message: 'Successfully enqueued token cleanup job.', jobId });
|
|
}
|
|
|
|
/**
|
|
* Trigger failing test job.
|
|
*
|
|
* Enqueues a test job designed to fail for testing retry mechanisms.
|
|
* Used for development and testing purposes only.
|
|
*
|
|
* @summary Trigger failing test job
|
|
* @param request Express request for logging
|
|
* @returns Job enqueued message with job ID
|
|
*/
|
|
@Post('trigger/failing-job')
|
|
@Middlewares(adminTriggerLimiter)
|
|
@SuccessResponse(202, 'Job enqueued')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async triggerFailingJob(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<JobTriggeredResponse>> {
|
|
const userProfile = request.user as UserProfile;
|
|
request.log.info(
|
|
`[Admin] Manual trigger for a failing job received from user: ${userProfile.user.user_id}`,
|
|
);
|
|
|
|
// Add a job with a special flag that the worker will recognize as a deliberate failure
|
|
const job = await analyticsQueue.add('generate-daily-report', { reportDate: 'FAIL' });
|
|
this.setStatus(202);
|
|
return this.success({
|
|
message: `Failing test job has been enqueued successfully. Job ID: ${job.id}`,
|
|
jobId: job.id,
|
|
});
|
|
}
|
|
|
|
// ==========================================================================
|
|
// SYSTEM OPERATIONS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Clear geocode cache.
|
|
*
|
|
* Clears all geocoded address data from Redis cache.
|
|
*
|
|
* @summary Clear geocode cache
|
|
* @param request Express request for logging
|
|
* @returns Cache clear result
|
|
*/
|
|
@Post('system/clear-geocode-cache')
|
|
@Middlewares(adminTriggerLimiter)
|
|
@SuccessResponse(200, 'Cache cleared')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async clearGeocodeCache(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<CacheClearResponse>> {
|
|
const userProfile = request.user as UserProfile;
|
|
request.log.info(
|
|
`[Admin] Manual trigger for geocode cache clear received from user: ${userProfile.user.user_id}`,
|
|
);
|
|
|
|
const keysDeleted = await geocodingService.clearGeocodeCache(request.log);
|
|
return this.success({
|
|
message: `Successfully cleared the geocode cache. ${keysDeleted} keys were removed.`,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear application cache.
|
|
*
|
|
* Clears cached flyers, brands, and stats data from Redis.
|
|
*
|
|
* @summary Clear application cache
|
|
* @param request Express request for logging
|
|
* @returns Cache clear result with details
|
|
*/
|
|
@Post('system/clear-cache')
|
|
@Middlewares(adminTriggerLimiter)
|
|
@SuccessResponse(200, 'Cache cleared')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
@Response<ErrorResponse>(429, 'Too many requests')
|
|
public async clearApplicationCache(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<CacheClearResponse>> {
|
|
const userProfile = request.user as UserProfile;
|
|
request.log.info(`[Admin] Manual cache clear received from user: ${userProfile.user.user_id}`);
|
|
|
|
const [flyersDeleted, brandsDeleted, statsDeleted] = await Promise.all([
|
|
cacheService.invalidateFlyers(request.log),
|
|
cacheService.invalidateBrands(request.log),
|
|
cacheService.invalidateStats(request.log),
|
|
]);
|
|
|
|
const totalDeleted = flyersDeleted + brandsDeleted + statsDeleted;
|
|
return this.success({
|
|
message: `Successfully cleared the application cache. ${totalDeleted} keys were removed.`,
|
|
details: {
|
|
flyers: flyersDeleted,
|
|
brands: brandsDeleted,
|
|
stats: statsDeleted,
|
|
},
|
|
});
|
|
}
|
|
|
|
// ==========================================================================
|
|
// FEATURE FLAGS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get feature flags.
|
|
*
|
|
* Returns the current state of all feature flags.
|
|
* See ADR-024 for feature flag documentation.
|
|
*
|
|
* @summary Get feature flags
|
|
* @returns Feature flags and their states
|
|
*/
|
|
@Get('feature-flags')
|
|
@SuccessResponse(200, 'Feature flags')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getFeatureFlags(
|
|
@Request() _request: ExpressRequest,
|
|
): Promise<SuccessResponseType<FeatureFlagsResponse>> {
|
|
const flags = getFeatureFlags();
|
|
return this.success({ flags });
|
|
}
|
|
|
|
// ==========================================================================
|
|
// WEBSOCKET STATISTICS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get WebSocket statistics.
|
|
*
|
|
* Returns real-time WebSocket connection statistics.
|
|
* See ADR-022 for WebSocket implementation details.
|
|
*
|
|
* @summary Get WebSocket stats
|
|
* @param request Express request for logging
|
|
* @returns Connection statistics
|
|
*/
|
|
@Get('websocket/stats')
|
|
@SuccessResponse(200, 'WebSocket statistics')
|
|
@Response<ErrorResponse>(401, 'Unauthorized')
|
|
@Response<ErrorResponse>(403, 'Forbidden - Admin role required')
|
|
public async getWebSocketStats(
|
|
@Request() _request: ExpressRequest,
|
|
): Promise<SuccessResponseType<WebSocketStatsDto>> {
|
|
const { websocketService } = await import('../services/websocketService.server');
|
|
const stats = websocketService.getConnectionStats();
|
|
return this.success(stats);
|
|
}
|
|
}
|