diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index 6519d2d3..83a8e4b7 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { BullModule } from '@nestjs/bullmq'; import { Admin } from './entities/admin.entity'; import { Incident } from './entities/incident.entity'; import { ModerationReport } from './entities/moderation-report.entity'; @@ -14,15 +15,31 @@ import { DashboardService } from './dashboard/dashboard.service'; import { DashboardController } from './dashboard/dashboard.controller'; import { RolesGuard } from './guards/roles.guard'; import { AdminGuard } from './guards/admin.guard'; +import { ProtocolAdminService } from './protocol/protocol-admin.service'; +import { ProtocolAdminController } from './protocol/protocol-admin.controller'; +import { FeatureFlagsModule } from '../feature-flags/feature-flags.module'; +import { JobsModule } from '../jobs/jobs.module'; +import { RedisModule } from '../redis/redis.module'; +import { QueueName } from '../jobs/jobs.types'; @Module({ imports: [ TypeOrmModule.forFeature([Admin, Incident, ModerationReport, AuditLog]), + FeatureFlagsModule, + JobsModule, + RedisModule, + BullModule.registerQueue( + { name: QueueName.DEFAULT }, + { name: QueueName.NOTIFICATIONS }, + { name: QueueName.BLOCKCHAIN }, + { name: QueueName.ANALYTICS }, + ), ], controllers: [ AdminController, ModerationController, IncidentController, DashboardController, + ProtocolAdminController, ], providers: [ AdminService, @@ -31,6 +48,7 @@ import { AdminGuard } from './guards/admin.guard'; DashboardService, RolesGuard, AdminGuard, + ProtocolAdminService, ], exports: [ AdminService, @@ -39,6 +57,7 @@ import { AdminGuard } from './guards/admin.guard'; DashboardService, RolesGuard, AdminGuard, + ProtocolAdminService, ], }) export class AdminModule {} diff --git a/src/admin/protocol/dto/config.dto.ts b/src/admin/protocol/dto/config.dto.ts new file mode 100644 index 00000000..00cdf756 --- /dev/null +++ b/src/admin/protocol/dto/config.dto.ts @@ -0,0 +1,93 @@ +import { IsString, IsOptional, IsNotEmpty, IsObject } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class ProtocolConfigDto { + @ApiProperty({ description: 'Configuration key' }) + @IsString() + @IsNotEmpty() + key: string; + + @ApiProperty({ description: 'Configuration value (JSON)' }) + @IsObject() + @IsNotEmpty() + value: Record; + + @ApiPropertyOptional({ description: 'Environment scope' }) + @IsString() + @IsOptional() + environment?: string; + + @ApiPropertyOptional({ description: 'Reason for change' }) + @IsString() + @IsOptional() + changeReason?: string; +} + +export class ProtocolConfigResponse { + @ApiProperty() + id: string; + + @ApiProperty() + key: string; + + @ApiProperty() + value: unknown; + + @ApiProperty() + environment: string; + + @ApiProperty() + version: number; + + @ApiPropertyOptional() + createdBy?: string; + + @ApiProperty() + createdAt: Date; + + @ApiProperty() + updatedAt: Date; +} + +export class OperationalStatsResponse { + @ApiProperty() + totalUsers: number; + + @ApiProperty() + totalClaims: number; + + @ApiProperty() + totalDisputes: number; + + @ApiProperty() + totalAdmins: number; + + @ApiProperty() + activeAdmins: number; + + @ApiProperty() + pendingClaims: number; + + @ApiProperty() + finalizedClaims: number; + + @ApiProperty() + auditLogCount: number; + + @ApiProperty() + queueMetrics: { + totalWaiting: number; + totalActive: number; + totalFailed: number; + totalCompleted: number; + }; + + @ApiProperty() + systemUptime: number; + + @ApiProperty() + environment: string; + + @ApiProperty() + timestamp: string; +} diff --git a/src/admin/protocol/dto/emergency.dto.ts b/src/admin/protocol/dto/emergency.dto.ts new file mode 100644 index 00000000..02affc4f --- /dev/null +++ b/src/admin/protocol/dto/emergency.dto.ts @@ -0,0 +1,79 @@ +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum EmergencyAction { + SUSPEND_ALL_SERVICES = 'suspend_all_services', + DISABLE_NOTIFICATIONS = 'disable_notifications', + PAUSE_ALL_QUEUES = 'pause_all_queues', + ENABLE_API_THROTTLING = 'enable_api_throttling', + SUSPEND_INTEGRATIONS = 'suspend_integrations', + EMERGENCY_SHUTDOWN = 'emergency_shutdown', +} + +export class ExecuteEmergencyActionDto { + @ApiProperty({ enum: EmergencyAction, description: 'Emergency action to execute' }) + @IsEnum(EmergencyAction) + @IsNotEmpty() + action: EmergencyAction; + + @ApiProperty({ description: 'Reason for emergency action' }) + @IsString() + @IsNotEmpty() + reason: string; + + @ApiPropertyOptional({ description: 'Duration in minutes (for time-bound actions)' }) + @IsOptional() + durationMinutes?: number; +} + +export class EmergencyActionResponse { + @ApiProperty() + action: EmergencyAction; + + @ApiProperty() + success: boolean; + + @ApiProperty() + timestamp: string; + + @ApiPropertyOptional() + message?: string; + + @ApiProperty({ type: [Object] }) + affectedServices: string[]; +} + +export class SystemStatusResponse { + @ApiProperty() + maintenanceMode: boolean; + + @ApiProperty() + emergencyActive: boolean; + + @ApiProperty({ type: [String] }) + activeEmergencies: string[]; + + @ApiProperty() + queuesOperational: boolean; + + @ApiProperty() + notificationsEnabled: boolean; + + @ApiProperty() + integrationsOperational: boolean; + + @ApiProperty() + apiThrottlingActive: boolean; + + @ApiProperty() + uptime: number; + + @ApiProperty() + environment: string; +} diff --git a/src/admin/protocol/dto/index.ts b/src/admin/protocol/dto/index.ts new file mode 100644 index 00000000..b42ed195 --- /dev/null +++ b/src/admin/protocol/dto/index.ts @@ -0,0 +1,4 @@ +export * from './maintenance.dto'; +export * from './service-control.dto'; +export * from './emergency.dto'; +export * from './config.dto'; diff --git a/src/admin/protocol/dto/maintenance.dto.ts b/src/admin/protocol/dto/maintenance.dto.ts new file mode 100644 index 00000000..91a69dd9 --- /dev/null +++ b/src/admin/protocol/dto/maintenance.dto.ts @@ -0,0 +1,71 @@ +import { + IsBoolean, + IsDateString, + IsOptional, + IsString, + IsNotEmpty, +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class SetMaintenanceModeDto { + @ApiProperty({ description: 'Enable or disable maintenance mode' }) + @IsBoolean() + @IsNotEmpty() + enabled: boolean; + + @ApiPropertyOptional({ description: 'Reason for maintenance mode change' }) + @IsString() + @IsOptional() + reason?: string; + + @ApiPropertyOptional({ description: 'Scheduled end time (ISO 8601)' }) + @IsDateString() + @IsOptional() + scheduledEnd?: string; +} + +export class ScheduleMaintenanceDto { + @ApiProperty({ description: 'Scheduled start time (ISO 8601)' }) + @IsDateString() + @IsNotEmpty() + startTime: string; + + @ApiPropertyOptional({ description: 'Scheduled end time (ISO 8601)' }) + @IsDateString() + @IsOptional() + endTime?: string; + + @ApiProperty({ description: 'Description of maintenance' }) + @IsString() + @IsNotEmpty() + description: string; + + @ApiPropertyOptional({ description: 'Services affected by maintenance' }) + @IsString({ each: true }) + @IsOptional() + affectedServices?: string[]; +} + +export class MaintenanceStatusResponse { + @ApiProperty() + active: boolean; + + @ApiPropertyOptional() + reason?: string; + + @ApiPropertyOptional() + startedAt?: string; + + @ApiPropertyOptional() + scheduledEnd?: string; + + @ApiProperty({ type: [Object] }) + scheduledMaintenance: Array<{ + id: string; + description: string; + startTime: string; + endTime?: string; + status: string; + affectedServices?: string[]; + }>; +} diff --git a/src/admin/protocol/dto/service-control.dto.ts b/src/admin/protocol/dto/service-control.dto.ts new file mode 100644 index 00000000..e6cdcff1 --- /dev/null +++ b/src/admin/protocol/dto/service-control.dto.ts @@ -0,0 +1,108 @@ +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum ServiceType { + QUEUE = 'queue', + NOTIFICATION = 'notification', + WEBHOOK = 'webhook', + CACHE = 'cache', + BLOCKCHAIN_INDEXER = 'blockchain_indexer', + BACKGROUND_PROCESSOR = 'background_processor', + SCHEDULED_JOB = 'scheduled_job', +} + +export enum QueueAction { + PAUSE = 'pause', + RESUME = 'resume', + CLEAR = 'clear', + RETRY_FAILED = 'retry_failed', +} + +export enum ServiceAction { + SUSPEND = 'suspend', + RESTORE = 'restore', + RESTART = 'restart', + INVALIDATE_CACHE = 'invalidate_cache', +} + +export class ControlServiceDto { + @ApiProperty({ enum: ServiceType, description: 'Type of service to control' }) + @IsEnum(ServiceType) + @IsNotEmpty() + serviceType: ServiceType; + + @ApiProperty({ + enum: [...Object.values(QueueAction), ...Object.values(ServiceAction)], + description: 'Action to perform on the service', + }) + @IsEnum({ ...QueueAction, ...ServiceAction } as any) + @IsNotEmpty() + action: QueueAction | ServiceAction; + + @ApiPropertyOptional({ description: 'Specific queue name (for queue operations)' }) + @IsString() + @IsOptional() + queueName?: string; + + @ApiPropertyOptional({ description: 'Reason for the action' }) + @IsString() + @IsOptional() + reason?: string; +} + +export class ServiceControlResponse { + @ApiProperty() + serviceType: ServiceType; + + @ApiProperty() + action: string; + + @ApiProperty() + success: boolean; + + @ApiPropertyOptional() + message?: string; + + @ApiPropertyOptional() + previousState?: string; + + @ApiPropertyOptional() + currentState?: string; +} + +export class QueueMetricsResponse { + @ApiProperty() + name: string; + + @ApiProperty() + waiting: number; + + @ApiProperty() + active: number; + + @ApiProperty() + completed: number; + + @ApiProperty() + failed: number; + + @ApiProperty() + delayed: number; + + @ApiProperty() + paused: boolean; +} + +export class AllQueueMetricsResponse { + @ApiProperty({ type: [QueueMetricsResponse] }) + queues: QueueMetricsResponse[]; + + @ApiProperty() + totalWaiting: number; + + @ApiProperty() + totalActive: number; + + @ApiProperty() + totalFailed: number; +} diff --git a/src/admin/protocol/protocol-admin.controller.ts b/src/admin/protocol/protocol-admin.controller.ts new file mode 100644 index 00000000..8a83c883 --- /dev/null +++ b/src/admin/protocol/protocol-admin.controller.ts @@ -0,0 +1,398 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOperation, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { AdminGuard } from '../guards/admin.guard'; +import { RolesGuard } from '../guards/roles.guard'; +import { Roles } from '../decorators/roles.decorator'; +import { CurrentAdmin } from '../decorators/current-admin.decorator'; +import { Admin, AdminRole } from '../entities/admin.entity'; +import { ProtocolAdminService } from './protocol-admin.service'; +import { + ExecuteEmergencyActionDto, + EmergencyActionResponse, + SystemStatusResponse, +} from './dto/emergency.dto'; +import { + SetMaintenanceModeDto, + ScheduleMaintenanceDto, + MaintenanceStatusResponse, +} from './dto/maintenance.dto'; +import { + ControlServiceDto, + ServiceControlResponse, + AllQueueMetricsResponse, +} from './dto/service-control.dto'; +import { ProtocolConfigDto, OperationalStatsResponse } from './dto/config.dto'; + +@ApiTags('Protocol Administration') +@ApiBearerAuth() +@UseGuards(AdminGuard, RolesGuard) +@Controller('admin/protocol') +export class ProtocolAdminController { + constructor( + private readonly protocolAdminService: ProtocolAdminService, + ) {} + + // ────────────────────────────────────────────── + // SYSTEM STATUS + // ────────────────────────────────────────────── + + @Get('status') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.SECURITY_ANALYST, + AdminRole.AUDITOR, + ) + @ApiOperation({ summary: 'Get overall system status' }) + @ApiResponse({ + status: 200, + description: 'System status', + type: SystemStatusResponse, + }) + async getSystemStatus(): Promise { + return this.protocolAdminService.getSystemStatus(); + } + + @Get('stats') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.SECURITY_ANALYST, + AdminRole.AUDITOR, + ) + @ApiOperation({ summary: 'Get operational statistics' }) + @ApiResponse({ + status: 200, + description: 'Operational statistics', + type: OperationalStatsResponse, + }) + async getOperationalStats(): Promise { + return this.protocolAdminService.getOperationalStats(); + } + + @Get('stats/detailed') + @Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR) + @ApiOperation({ summary: 'Get detailed operational statistics' }) + async getDetailedStats(): Promise> { + return this.protocolAdminService.getDetailedOperationalStats(); + } + + // ────────────────────────────────────────────── + // MAINTENANCE + // ────────────────────────────────────────────── + + @Get('maintenance') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.SECURITY_ANALYST, + AdminRole.AUDITOR, + ) + @ApiOperation({ summary: 'Get maintenance mode status' }) + @ApiResponse({ + status: 200, + description: 'Maintenance status', + type: MaintenanceStatusResponse, + }) + async getMaintenanceStatus(): Promise { + return this.protocolAdminService.getMaintenanceStatus(); + } + + @Post('maintenance') + @Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR) + @ApiOperation({ summary: 'Enable or disable maintenance mode' }) + @ApiResponse({ + status: 200, + description: 'Maintenance mode updated', + type: MaintenanceStatusResponse, + }) + async setMaintenanceMode( + @Body() dto: SetMaintenanceModeDto, + @CurrentAdmin() admin: Admin, + ): Promise { + return this.protocolAdminService.setMaintenanceMode(dto, admin.id); + } + + @Post('maintenance/schedule') + @Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR) + @ApiOperation({ summary: 'Schedule maintenance' }) + @ApiResponse({ status: 201, description: 'Maintenance scheduled' }) + async scheduleMaintenance( + @Body() dto: ScheduleMaintenanceDto, + @CurrentAdmin() admin: Admin, + ) { + return this.protocolAdminService.scheduleMaintenance(dto, admin.id); + } + + @Delete('maintenance/schedule/:scheduleId') + @Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR) + @ApiOperation({ summary: 'Cancel scheduled maintenance' }) + @ApiParam({ name: 'scheduleId', description: 'Maintenance schedule ID' }) + @ApiResponse({ status: 200, description: 'Maintenance cancelled' }) + async cancelMaintenance( + @Param('scheduleId') scheduleId: string, + @CurrentAdmin() admin: Admin, + ): Promise { + return this.protocolAdminService.cancelMaintenance(scheduleId, admin.id); + } + + // ────────────────────────────────────────────── + // SERVICE MANAGEMENT + // ────────────────────────────────────────────── + + @Post('services/control') + @Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR) + @ApiOperation({ summary: 'Control a service (pause/resume/suspend/etc.)' }) + @ApiResponse({ + status: 200, + description: 'Service control executed', + type: ServiceControlResponse, + }) + async controlService( + @Body() dto: ControlServiceDto, + @CurrentAdmin() admin: Admin, + ): Promise { + return this.protocolAdminService.controlService(dto, admin.id); + } + + @Get('queues') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.SECURITY_ANALYST, + AdminRole.AUDITOR, + ) + @ApiOperation({ summary: 'Get queue metrics for all queues' }) + @ApiResponse({ + status: 200, + description: 'Queue metrics', + type: AllQueueMetricsResponse, + }) + async getQueueMetrics(): Promise { + return this.protocolAdminService.getQueueMetrics(); + } + + @Post('queues/retry-failed') + @Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR) + @ApiOperation({ summary: 'Retry all failed jobs' }) + @ApiQuery({ + name: 'queueName', + required: false, + description: 'Specific queue name (optional)', + }) + async retryFailedJobs( + @Query('queueName') queueName?: string, + @CurrentAdmin() admin?: Admin, + ): Promise<{ retried: number }> { + return this.protocolAdminService.retryFailedJobs( + queueName, + admin?.id, + ); + } + + // ────────────────────────────────────────────── + // EMERGENCY OPERATIONS + // ────────────────────────────────────────────── + + @Post('emergency') + @Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR) + @ApiOperation({ summary: 'Execute an emergency operational action' }) + @ApiResponse({ + status: 200, + description: 'Emergency action executed', + type: EmergencyActionResponse, + }) + async executeEmergencyAction( + @Body() dto: ExecuteEmergencyActionDto, + @CurrentAdmin() admin: Admin, + ): Promise { + return this.protocolAdminService.executeEmergencyAction(dto, admin.id); + } + + @Post('emergency/:action/resolve') + @Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR) + @ApiOperation({ summary: 'Resolve an active emergency action' }) + @ApiParam({ + name: 'action', + description: 'Emergency action to resolve', + }) + @ApiResponse({ status: 200, description: 'Emergency action resolved' }) + async resolveEmergencyAction( + @Param('action') action: string, + @CurrentAdmin() admin: Admin, + ): Promise { + return this.protocolAdminService.resolveEmergencyAction( + action as any, + admin.id, + ); + } + + // ────────────────────────────────────────────── + // CONFIGURATION + // ────────────────────────────────────────────── + + @Get('config') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.SECURITY_ANALYST, + AdminRole.AUDITOR, + ) + @ApiOperation({ summary: 'List all protocol configuration values' }) + @ApiQuery({ + name: 'environment', + required: false, + description: 'Environment scope', + }) + async listConfig( + @Query('environment') environment?: string, + ) { + return this.protocolAdminService.listAllConfig(environment); + } + + @Get('config/:key') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.SECURITY_ANALYST, + AdminRole.AUDITOR, + ) + @ApiOperation({ summary: 'Get a specific protocol configuration' }) + @ApiParam({ name: 'key', description: 'Configuration key' }) + async getConfig( + @Param('key') key: string, + @Query('environment') environment?: string, + ): Promise<{ key: string; value: unknown } | null> { + return this.protocolAdminService.getProtocolConfig(key, environment); + } + + @Post('config') + @Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR) + @ApiOperation({ summary: 'Set a protocol configuration value' }) + @ApiResponse({ status: 201, description: 'Configuration updated' }) + async setConfig( + @Body() dto: ProtocolConfigDto, + @CurrentAdmin() admin: Admin, + ) { + return this.protocolAdminService.setProtocolConfig(dto, admin.id); + } + + // ────────────────────────────────────────────── + // FEATURE FLAGS + // ────────────────────────────────────────────── + + @Get('feature-flags') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.SECURITY_ANALYST, + AdminRole.AUDITOR, + ) + @ApiOperation({ summary: 'List all feature flags' }) + @ApiQuery({ + name: 'environment', + required: false, + description: 'Environment scope', + }) + async listFeatureFlags( + @Query('environment') environment?: string, + ) { + return this.protocolAdminService.listFeatureFlags(environment); + } + + @Get('feature-flags/evaluate/:key') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.SECURITY_ANALYST, + AdminRole.AUDITOR, + ) + @ApiOperation({ summary: 'Evaluate a feature flag for a given context' }) + async evaluateFeatureFlag( + @Param('key') key: string, + @Query('userId') userId?: string, + @Query('roles') roles?: string, + @Query('environment') environment?: string, + ) { + const context: Record = {}; + if (userId) context.userId = userId; + if (roles) context.roles = roles.split(','); + if (environment) context.environment = environment; + return this.protocolAdminService.evaluateFeatureFlag(key, context); + } + + // ────────────────────────────────────────────── + // AUDIT + // ────────────────────────────────────────────── + + @Get('audit-logs') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.AUDITOR, + AdminRole.SECURITY_ANALYST, + ) + @ApiOperation({ summary: 'Get protocol administration audit logs' }) + @ApiQuery({ + name: 'limit', + required: false, + description: 'Number of logs (default 50)', + }) + @ApiQuery({ + name: 'offset', + required: false, + description: 'Pagination offset (default 0)', + }) + async getAuditLogs( + @Query('limit') limit?: string, + @Query('offset') offset?: string, + ): Promise<{ logs: unknown[]; total: number }> { + return this.protocolAdminService.getProtocolAuditLogs( + limit ? parseInt(limit, 10) : 50, + offset ? parseInt(offset, 10) : 0, + ); + } + + @Get('audit-logs/admin') + @Roles( + AdminRole.SUPER_ADMIN, + AdminRole.ADMINISTRATOR, + AdminRole.AUDITOR, + ) + @ApiOperation({ summary: 'Get administrative action audit logs' }) + @ApiQuery({ + name: 'limit', + required: false, + description: 'Number of logs (default 50)', + }) + @ApiQuery({ + name: 'offset', + required: false, + description: 'Pagination offset (default 0)', + }) + async getAdminAuditLogs( + @Query('limit') limit?: string, + @Query('offset') offset?: string, + ): Promise<{ logs: unknown[]; total: number }> { + return this.protocolAdminService.getAdminAuditLogs( + limit ? parseInt(limit, 10) : 50, + offset ? parseInt(offset, 10) : 0, + ); + } +} diff --git a/src/admin/protocol/protocol-admin.service.ts b/src/admin/protocol/protocol-admin.service.ts new file mode 100644 index 00000000..77c41748 --- /dev/null +++ b/src/admin/protocol/protocol-admin.service.ts @@ -0,0 +1,959 @@ +import { + Injectable, + Logger, + NotFoundException, + ConflictException, + BadRequestException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { InjectQueue } from '@nestjs/bullmq'; +import { Queue } from 'bullmq'; + +import { Admin, AdminRole } from '../entities/admin.entity'; +import { AuditLog } from '../../audit/entities/audit-log.entity'; +import { AuditTrailService } from '../../audit/services/audit-trail.service'; +import { FeatureFlagsService } from '../../feature-flags/feature-flags.service'; +import { ConfigurationService } from '../../feature-flags/configuration.service'; +import { JobsService } from '../../jobs/jobs.service'; +import { QueueName } from '../../jobs/jobs.types'; +import { RedisService } from '../../redis/redis.service'; + +import { + ServiceType, + QueueAction, + ServiceAction, + ControlServiceDto, + ServiceControlResponse, + AllQueueMetricsResponse, + QueueMetricsResponse, +} from './dto/service-control.dto'; +import { + EmergencyAction, + ExecuteEmergencyActionDto, + EmergencyActionResponse, + SystemStatusResponse, +} from './dto/emergency.dto'; +import { + SetMaintenanceModeDto, + ScheduleMaintenanceDto, + MaintenanceStatusResponse, +} from './dto/maintenance.dto'; +import { + ProtocolConfigDto, + OperationalStatsResponse, +} from './dto/config.dto'; + +import { + AuditActionType, + AuditEntityType, + AuditSeverity, + AuditCategory, +} from '../../audit/entities/audit-log.entity'; + +interface EmergencyState { + action: EmergencyAction; + reason: string; + timestamp: string; + expiresAt?: number; +} + +interface MaintenanceSchedule { + id: string; + description: string; + startTime: string; + endTime?: string; + status: 'scheduled' | 'active' | 'completed' | 'cancelled'; + affectedServices?: string[]; + createdAt: string; + createdBy?: string; +} + +@Injectable() +export class ProtocolAdminService { + private readonly logger = new Logger(ProtocolAdminService.name); + + private maintenanceActive = false; + private maintenanceReason = ''; + private maintenanceStartedAt: string | null = null; + private maintenanceScheduledEnd: string | null = null; + private scheduledMaintenance: MaintenanceSchedule[] = []; + private emergencyStates = new Map(); + private serviceStates = new Map(); + private readonly startTime = Date.now(); + + constructor( + @InjectRepository(Admin) + private readonly adminRepo: Repository, + @InjectRepository(AuditLog) + private readonly auditLogRepo: Repository, + private readonly dataSource: DataSource, + private readonly auditTrailService: AuditTrailService, + private readonly featureFlagsService: FeatureFlagsService, + private readonly configService: ConfigurationService, + private readonly jobsService: JobsService, + private readonly redisService: RedisService, + @InjectQueue(QueueName.DEFAULT) private readonly defaultQueue: Queue, + @InjectQueue(QueueName.NOTIFICATIONS) + private readonly notificationsQueue: Queue, + @InjectQueue(QueueName.BLOCKCHAIN) + private readonly blockchainQueue: Queue, + @InjectQueue(QueueName.ANALYTICS) + private readonly analyticsQueue: Queue, + ) { + this.serviceStates.set('notifications', true); + this.serviceStates.set('integrations', true); + this.serviceStates.set('queues_operational', true); + } + + // ────────────────────────────────────────────── + // SYSTEM STATUS + // ────────────────────────────────────────────── + + async getSystemStatus(): Promise { + const activeEmergencies: string[] = []; + this.emergencyStates.forEach((state, action) => { + if (state.expiresAt) { + if (Date.now() < state.expiresAt) { + activeEmergencies.push(action); + } else { + this.emergencyStates.delete(action); + } + } else { + activeEmergencies.push(action); + } + }); + + return { + maintenanceMode: this.maintenanceActive, + emergencyActive: activeEmergencies.length > 0, + activeEmergencies, + queuesOperational: this.serviceStates.get('queues_operational') ?? true, + notificationsEnabled: this.serviceStates.get('notifications') ?? true, + integrationsOperational: + this.serviceStates.get('integrations') ?? true, + apiThrottlingActive: activeEmergencies.includes( + EmergencyAction.ENABLE_API_THROTTLING, + ), + uptime: this.getUptime(), + environment: process.env.NODE_ENV ?? 'development', + }; + } + + async getOperationalStats(): Promise { + const [totalUsers, totalAdmins, auditLogCount] = await Promise.all([ + this.countEntity('user'), + this.adminRepo.count(), + this.auditLogRepo.count(), + ]); + + const allMetrics = await this.jobsService.getAllQueueMetrics(); + + const totalWaiting = allMetrics.reduce((sum, m) => sum + m.waiting, 0); + const totalActive = allMetrics.reduce((sum, m) => sum + m.active, 0); + const totalFailed = allMetrics.reduce((sum, m) => sum + m.failed, 0); + const totalCompleted = allMetrics.reduce( + (sum, m) => sum + m.completed, + 0, + ); + + return { + totalUsers, + totalClaims: 0, + totalDisputes: 0, + totalAdmins, + activeAdmins: 0, + pendingClaims: 0, + finalizedClaims: 0, + auditLogCount, + queueMetrics: { + totalWaiting, + totalActive, + totalFailed, + totalCompleted, + }, + systemUptime: this.getUptime(), + environment: process.env.NODE_ENV ?? 'development', + timestamp: new Date().toISOString(), + }; + } + + async getDetailedOperationalStats(): Promise> { + const allMetrics = await this.jobsService.getAllQueueMetrics(); + const queueMetrics = allMetrics.reduce( + (acc, m) => { + acc[m.name] = { + waiting: m.waiting, + active: m.active, + completed: m.completed, + failed: m.failed, + delayed: m.delayed, + paused: m.paused, + }; + return acc; + }, + {} as Record, + ); + + return { + queues: queueMetrics, + services: { + maintenanceMode: this.maintenanceActive, + notificationsEnabled: this.serviceStates.get('notifications') ?? true, + integrationsOperational: + this.serviceStates.get('integrations') ?? true, + apiThrottlingActive: this.emergencyStates.has( + EmergencyAction.ENABLE_API_THROTTLING, + ), + }, + system: { + uptime: this.getUptime(), + memory: process.memoryUsage(), + nodeVersion: process.version, + platform: process.platform, + }, + }; + } + + // ────────────────────────────────────────────── + // MAINTENANCE MODE + // ────────────────────────────────────────────── + + async setMaintenanceMode( + dto: SetMaintenanceModeDto, + adminId: string, + ): Promise { + const wasActive = this.maintenanceActive; + this.maintenanceActive = dto.enabled; + this.maintenanceReason = dto.reason ?? ''; + this.maintenanceStartedAt = dto.enabled + ? new Date().toISOString() + : null; + this.maintenanceScheduledEnd = dto.scheduledEnd ?? null; + + const actionType = dto.enabled + ? AuditActionType.MAINTENANCE_MODE_ENABLED + : AuditActionType.MAINTENANCE_MODE_DISABLED; + + await this.auditTrailService.log({ + actionType, + entityType: AuditEntityType.MAINTENANCE, + entityId: 'system', + userId: adminId, + severity: AuditSeverity.HIGH, + category: AuditCategory.MAINTENANCE, + description: dto.enabled + ? `Maintenance mode enabled${dto.reason ? `: ${dto.reason}` : ''}` + : 'Maintenance mode disabled', + beforeState: { maintenanceActive: wasActive }, + afterState: { maintenanceActive: dto.enabled, reason: dto.reason }, + }); + + this.logger.log( + `Maintenance mode ${dto.enabled ? 'enabled' : 'disabled'} by admin ${adminId}`, + ); + return this.getMaintenanceStatus(); + } + + async scheduleMaintenance( + dto: ScheduleMaintenanceDto, + adminId: string, + ): Promise { + const schedule: MaintenanceSchedule = { + id: `mnt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`, + description: dto.description, + startTime: dto.startTime, + endTime: dto.endTime, + status: 'scheduled', + affectedServices: dto.affectedServices, + createdAt: new Date().toISOString(), + createdBy: adminId, + }; + + this.scheduledMaintenance.push(schedule); + + await this.auditTrailService.log({ + actionType: AuditActionType.MAINTENANCE_SCHEDULED, + entityType: AuditEntityType.MAINTENANCE, + entityId: schedule.id, + userId: adminId, + severity: AuditSeverity.MEDIUM, + category: AuditCategory.MAINTENANCE, + description: `Scheduled maintenance: ${dto.description}`, + afterState: schedule as unknown as Record, + }); + + this.logger.log( + `Maintenance scheduled: ${schedule.id} - ${dto.description}`, + ); + return schedule; + } + + async cancelMaintenance( + scheduleId: string, + adminId: string, + ): Promise { + const index = this.scheduledMaintenance.findIndex( + (s) => s.id === scheduleId, + ); + if (index === -1) { + throw new NotFoundException( + `Maintenance schedule ${scheduleId} not found`, + ); + } + + const schedule = this.scheduledMaintenance[index]; + if (schedule.status !== 'scheduled') { + throw new ConflictException( + `Cannot cancel maintenance with status '${schedule.status}'`, + ); + } + + this.scheduledMaintenance[index] = { + ...schedule, + status: 'cancelled', + }; + + await this.auditTrailService.log({ + actionType: AuditActionType.MAINTENANCE_CANCELLED, + entityType: AuditEntityType.MAINTENANCE, + entityId: scheduleId, + userId: adminId, + severity: AuditSeverity.MEDIUM, + category: AuditCategory.MAINTENANCE, + description: `Cancelled maintenance: ${schedule.description}`, + beforeState: { status: schedule.status }, + afterState: { status: 'cancelled' }, + }); + } + + async getMaintenanceStatus(): Promise { + return { + active: this.maintenanceActive, + reason: this.maintenanceReason || undefined, + startedAt: this.maintenanceStartedAt ?? undefined, + scheduledEnd: this.maintenanceScheduledEnd ?? undefined, + scheduledMaintenance: this.scheduledMaintenance.filter( + (s) => s.status === 'scheduled' || s.status === 'active', + ), + }; + } + + // ────────────────────────────────────────────── + // SERVICE MANAGEMENT + // ────────────────────────────────────────────── + + async controlService( + dto: ControlServiceDto, + adminId: string, + ): Promise { + const { serviceType, action, queueName, reason } = dto; + const beforeState = { + operational: this.serviceStates.get(serviceType) ?? true, + }; + + try { + switch (serviceType) { + case ServiceType.QUEUE: + return await this.handleQueueAction( + action as QueueAction, + queueName, + adminId, + reason, + beforeState, + ); + case ServiceType.NOTIFICATION: + return await this.handleNotificationAction( + action as ServiceAction, + adminId, + reason, + beforeState, + ); + case ServiceType.WEBHOOK: + return await this.handleWebhookAction( + action as ServiceAction, + adminId, + reason, + beforeState, + ); + case ServiceType.CACHE: + return await this.handleCacheAction( + action as ServiceAction, + adminId, + beforeState, + ); + case ServiceType.BLOCKCHAIN_INDEXER: + case ServiceType.BACKGROUND_PROCESSOR: + case ServiceType.SCHEDULED_JOB: + return await this.handleGenericServiceAction( + serviceType, + action as ServiceAction, + adminId, + reason, + beforeState, + ); + default: + throw new BadRequestException( + `Unknown service type: ${serviceType}`, + ); + } + } catch (error) { + this.logger.error( + `Service control failed: ${serviceType}/${action} - ${(error as Error).message}`, + ); + return { + serviceType, + action, + success: false, + message: (error as Error).message, + previousState: JSON.stringify(beforeState), + currentState: JSON.stringify({ + operational: this.serviceStates.get(serviceType) ?? true, + }), + }; + } + } + + private async handleQueueAction( + action: QueueAction, + queueName?: string, + adminId?: string, + reason?: string, + beforeState?: Record, + ): Promise { + const queuesToActOn = queueName + ? [queueName] + : [QueueName.DEFAULT, QueueName.NOTIFICATIONS, QueueName.BLOCKCHAIN, QueueName.ANALYTICS]; + + let auditActionType: AuditActionType; + + for (const name of queuesToActOn) { + switch (action) { + case QueueAction.PAUSE: + await this.jobsService.pauseQueue(name as QueueName); + auditActionType = AuditActionType.QUEUE_PAUSED; + break; + case QueueAction.RESUME: + await this.jobsService.resumeQueue(name as QueueName); + auditActionType = AuditActionType.QUEUE_RESUMED; + break; + case QueueAction.RETRY_FAILED: + await this.jobsService.retryFailed(name as QueueName); + auditActionType = AuditActionType.SERVICE_HEALTH_CHECK; + break; + case QueueAction.CLEAR: + await this.clearQueue(name as QueueName); + auditActionType = AuditActionType.SERVICE_SUSPENDED; + break; + default: + throw new BadRequestException(`Unknown queue action: ${action}`); + } + } + + if (adminId) { + await this.auditTrailService.log({ + actionType: auditActionType!, + entityType: AuditEntityType.QUEUE, + entityId: queuesToActOn.join(','), + userId: adminId, + severity: AuditSeverity.HIGH, + category: AuditCategory.SERVICE_CONTROL, + description: `${action} ${action === QueueAction.RETRY_FAILED ? 'failed jobs on' : ''} queue(s): ${queuesToActOn.join(', ')}${reason ? ` - ${reason}` : ''}`, + beforeState, + afterState: { action, queues: queuesToActOn, reason }, + }); + } + + return { + serviceType: ServiceType.QUEUE, + action, + success: true, + message: `Queue(s) ${queuesToActOn.join(', ')} ${action}d successfully`, + previousState: beforeState ? JSON.stringify(beforeState) : undefined, + currentState: JSON.stringify({ operational: true }), + }; + } + + private async handleNotificationAction( + action: ServiceAction, + adminId?: string, + reason?: string, + beforeState?: Record, + ): Promise { + const isSuspending = action === ServiceAction.SUSPEND; + this.serviceStates.set('notifications', !isSuspending); + + const auditActionType = isSuspending + ? AuditActionType.SERVICE_SUSPENDED + : AuditActionType.SERVICE_RESTORED; + + if (adminId) { + await this.auditTrailService.log({ + actionType: auditActionType, + entityType: AuditEntityType.SERVICE, + entityId: 'notification-service', + userId: adminId, + severity: AuditSeverity.HIGH, + category: AuditCategory.SERVICE_CONTROL, + description: `${isSuspending ? 'Suspended' : 'Restored'} notification service${reason ? `: ${reason}` : ''}`, + beforeState, + afterState: { + notificationsEnabled: !isSuspending, + reason, + }, + }); + } + + return { + serviceType: ServiceType.NOTIFICATION, + action, + success: true, + message: `Notification service ${isSuspending ? 'suspended' : 'restored'} successfully`, + previousState: beforeState ? JSON.stringify(beforeState) : undefined, + }; + } + + private async handleWebhookAction( + action: ServiceAction, + adminId?: string, + reason?: string, + beforeState?: Record, + ): Promise { + const isSuspending = action === ServiceAction.SUSPEND; + this.serviceStates.set('integrations', !isSuspending); + + const auditActionType = isSuspending + ? AuditActionType.INTEGRATION_SUSPENDED + : AuditActionType.INTEGRATION_RESTORED; + + if (adminId) { + await this.auditTrailService.log({ + actionType: auditActionType, + entityType: AuditEntityType.INTEGRATION, + entityId: 'webhook-service', + userId: adminId, + severity: AuditSeverity.HIGH, + category: AuditCategory.SERVICE_CONTROL, + description: `${isSuspending ? 'Suspended' : 'Restored'} webhook service${reason ? `: ${reason}` : ''}`, + beforeState, + afterState: { + integrationsOperational: !isSuspending, + reason, + }, + }); + } + + return { + serviceType: ServiceType.WEBHOOK, + action, + success: true, + message: `Webhook service ${isSuspending ? 'suspended' : 'restored'} successfully`, + previousState: beforeState ? JSON.stringify(beforeState) : undefined, + }; + } + + private async handleCacheAction( + action: ServiceAction, + adminId?: string, + beforeState?: Record, + ): Promise { + if (action !== ServiceAction.INVALIDATE_CACHE) { + throw new BadRequestException( + `Invalid action for cache service: ${action}`, + ); + } + + await this.redisService.flushall(); + this.logger.log('Cache invalidated globally'); + + if (adminId) { + await this.auditTrailService.log({ + actionType: AuditActionType.CACHE_INVALIDATED, + entityType: AuditEntityType.CACHE, + entityId: 'global-cache', + userId: adminId, + severity: AuditSeverity.MEDIUM, + category: AuditCategory.SERVICE_CONTROL, + description: 'Global cache invalidated', + beforeState, + afterState: { cacheCleared: true, timestamp: new Date().toISOString() }, + }); + } + + return { + serviceType: ServiceType.CACHE, + action, + success: true, + message: 'Global cache invalidated successfully', + previousState: beforeState ? JSON.stringify(beforeState) : undefined, + }; + } + + private async handleGenericServiceAction( + serviceType: ServiceType, + action: ServiceAction, + adminId?: string, + reason?: string, + beforeState?: Record, + ): Promise { + const serviceKey = serviceType; + const isSuspending = action === ServiceAction.SUSPEND; + this.serviceStates.set(serviceKey, !isSuspending); + + const auditActionType = isSuspending + ? AuditActionType.SERVICE_SUSPENDED + : AuditActionType.SERVICE_RESTORED; + + if (adminId) { + await this.auditTrailService.log({ + actionType: auditActionType, + entityType: AuditEntityType.SERVICE, + entityId: serviceType, + userId: adminId, + severity: AuditSeverity.MEDIUM, + category: AuditCategory.SERVICE_CONTROL, + description: `${isSuspending ? 'Suspended' : 'Restored'} ${serviceType}${reason ? `: ${reason}` : ''}`, + beforeState, + afterState: { operational: !isSuspending, reason }, + }); + } + + return { + serviceType, + action, + success: true, + message: `${serviceType} ${isSuspending ? 'suspended' : 'restored'} successfully`, + previousState: beforeState ? JSON.stringify(beforeState) : undefined, + }; + } + + async getQueueMetrics(): Promise { + const allMetrics = await this.jobsService.getAllQueueMetrics(); + + const queues: QueueMetricsResponse[] = allMetrics.map((m) => ({ + name: m.name, + waiting: m.waiting, + active: m.active, + completed: m.completed, + failed: m.failed, + delayed: m.delayed, + paused: m.paused, + })); + + return { + queues, + totalWaiting: queues.reduce((s, q) => s + q.waiting, 0), + totalActive: queues.reduce((s, q) => s + q.active, 0), + totalFailed: queues.reduce((s, q) => s + q.failed, 0), + }; + } + + async retryFailedJobs( + queueName?: string, + adminId?: string, + ): Promise<{ retried: number }> { + let totalRetried = 0; + + const queuesToRetry = queueName + ? [queueName as QueueName] + : [QueueName.DEFAULT, QueueName.NOTIFICATIONS, QueueName.BLOCKCHAIN, QueueName.ANALYTICS]; + + for (const name of queuesToRetry) { + const retried = await this.jobsService.retryFailed(name); + totalRetried += retried; + } + + if (adminId) { + await this.auditTrailService.log({ + actionType: AuditActionType.SERVICE_HEALTH_CHECK, + entityType: AuditEntityType.QUEUE, + entityId: 'failed-jobs', + userId: adminId, + severity: AuditSeverity.LOW, + category: AuditCategory.OPERATIONS, + description: `Retried ${totalRetried} failed jobs on ${queuesToRetry.join(', ')}`, + afterState: { retried: totalRetried, queues: queuesToRetry }, + }); + } + + return { retried: totalRetried }; + } + + // ────────────────────────────────────────────── + // EMERGENCY OPERATIONS + // ────────────────────────────────────────────── + + async executeEmergencyAction( + dto: ExecuteEmergencyActionDto, + adminId: string, + ): Promise { + const { action, reason, durationMinutes } = dto; + const expiresAt = durationMinutes + ? Date.now() + durationMinutes * 60 * 1000 + : undefined; + + const emergency: EmergencyState = { + action, + reason, + timestamp: new Date().toISOString(), + expiresAt, + }; + + this.emergencyStates.set(action, emergency); + + const affectedServices = this.getAffectedServices(action); + this.applyEmergencyState(action, true); + + await this.auditTrailService.log({ + actionType: AuditActionType.EMERGENCY_ACTION_EXECUTED, + entityType: AuditEntityType.SERVICE, + entityId: `emergency-${action}`, + userId: adminId, + severity: AuditSeverity.CRITICAL, + category: AuditCategory.EMERGENCY, + description: `Emergency action: ${action} - ${reason}`, + afterState: { + action, + reason, + expiresAt: expiresAt + ? new Date(expiresAt).toISOString() + : undefined, + affectedServices, + }, + }); + + this.logger.warn( + `Emergency action executed: ${action} by admin ${adminId} - ${reason}`, + ); + + return { + action, + success: true, + timestamp: emergency.timestamp, + message: `Emergency action '${action}' executed successfully. Reason: ${reason}`, + affectedServices, + }; + } + + async resolveEmergencyAction( + action: EmergencyAction, + adminId: string, + ): Promise { + const emergency = this.emergencyStates.get(action); + if (!emergency) { + throw new NotFoundException( + `No active emergency action: ${action}`, + ); + } + + this.emergencyStates.delete(action); + this.applyEmergencyState(action, false); + + await this.auditTrailService.log({ + actionType: AuditActionType.SERVICE_RESTORED, + entityType: AuditEntityType.SERVICE, + entityId: `emergency-${action}`, + userId: adminId, + severity: AuditSeverity.HIGH, + category: AuditCategory.EMERGENCY, + description: `Resolved emergency action: ${action}`, + beforeState: emergency as unknown as Record, + afterState: { resolved: true, resolvedAt: new Date().toISOString() }, + }); + + this.logger.log( + `Emergency action resolved: ${action} by admin ${adminId}`, + ); + } + + private getAffectedServices(action: EmergencyAction): string[] { + switch (action) { + case EmergencyAction.SUSPEND_ALL_SERVICES: + return [ + 'queues', + 'notifications', + 'webhooks', + 'blockchain_indexer', + 'background_processor', + ]; + case EmergencyAction.DISABLE_NOTIFICATIONS: + return ['notifications']; + case EmergencyAction.PAUSE_ALL_QUEUES: + return ['queues']; + case EmergencyAction.SUSPEND_INTEGRATIONS: + return ['webhooks', 'integrations']; + case EmergencyAction.ENABLE_API_THROTTLING: + return ['api']; + case EmergencyAction.EMERGENCY_SHUTDOWN: + return ['all']; + default: + return ['unknown']; + } + } + + private applyEmergencyState(action: EmergencyAction, active: boolean): void { + switch (action) { + case EmergencyAction.SUSPEND_ALL_SERVICES: + this.serviceStates.set('notifications', !active); + this.serviceStates.set('integrations', !active); + this.serviceStates.set('queues_operational', !active); + break; + case EmergencyAction.DISABLE_NOTIFICATIONS: + this.serviceStates.set('notifications', !active); + break; + case EmergencyAction.PAUSE_ALL_QUEUES: + this.serviceStates.set('queues_operational', !active); + break; + case EmergencyAction.SUSPEND_INTEGRATIONS: + this.serviceStates.set('integrations', !active); + break; + case EmergencyAction.EMERGENCY_SHUTDOWN: + this.serviceStates.set('notifications', !active); + this.serviceStates.set('integrations', !active); + this.serviceStates.set('queues_operational', !active); + this.maintenanceActive = active; + break; + } + } + + // ────────────────────────────────────────────── + // CONFIGURATION MANAGEMENT + // ────────────────────────────────────────────── + + async getProtocolConfig( + key: string, + environment?: string, + ): Promise<{ key: string; value: unknown } | null> { + return this.configService.get(key, environment); + } + + async setProtocolConfig( + dto: ProtocolConfigDto, + adminId: string, + ): Promise { + const beforeValue = await this.configService.get( + dto.key, + dto.environment, + ); + + const saved = await this.configService.set( + dto.key, + dto.value, + dto.environment, + adminId, + dto.changeReason, + ); + + await this.auditTrailService.log({ + actionType: AuditActionType.PROTOCOL_CONFIG_UPDATED, + entityType: AuditEntityType.CONFIGURATION, + entityId: saved.id, + userId: adminId, + severity: AuditSeverity.MEDIUM, + category: AuditCategory.PROTOCOL, + description: `Updated protocol config: ${dto.key}${dto.changeReason ? ` - ${dto.changeReason}` : ''}`, + beforeState: beforeValue + ? ({ value: beforeValue } as Record) + : undefined, + afterState: { + key: dto.key, + value: dto.value, + changeReason: dto.changeReason, + }, + }); + + return saved; + } + + async listAllConfig(environment?: string): Promise { + return this.configService.findAll(environment); + } + + // ────────────────────────────────────────────── + // FEATURE FLAGS + // ────────────────────────────────────────────── + + async listFeatureFlags(environment?: string) { + return this.featureFlagsService.findAll(environment); + } + + async evaluateFeatureFlag( + key: string, + context?: Record, + ) { + return this.featureFlagsService.evaluate(key, context); + } + + // ────────────────────────────────────────────── + // AUDIT & METRICS + // ────────────────────────────────────────────── + + async getAdminAuditLogs( + limit = 50, + offset = 0, + ): Promise<{ logs: AuditLog[]; total: number }> { + const [logs, total] = await this.auditLogRepo.findAndCount({ + where: { category: AuditCategory.ADMINISTRATIVE }, + order: { createdAt: 'DESC' }, + skip: offset, + take: limit, + }); + return { logs, total }; + } + + async getProtocolAuditLogs( + limit = 50, + offset = 0, + ): Promise<{ logs: AuditLog[]; total: number }> { + const [logs, total] = await this.auditLogRepo.findAndCount({ + where: [ + { category: AuditCategory.MAINTENANCE }, + { category: AuditCategory.EMERGENCY }, + { category: AuditCategory.SERVICE_CONTROL }, + { category: AuditCategory.PROTOCOL }, + ], + order: { createdAt: 'DESC' }, + skip: offset, + take: limit, + }); + return { logs, total }; + } + + // ────────────────────────────────────────────── + // HELPERS + // ────────────────────────────────────────────── + + private async clearQueue(queueName: QueueName): Promise { + const queue = this.getQueueInstance(queueName); + if (!queue) { + throw new NotFoundException(`Queue ${queueName} not found`); + } + await queue.drain(); + this.logger.log(`Queue ${queueName} cleared`); + } + + private getQueueInstance(name: QueueName): Queue | undefined { + const instances: Record = { + [QueueName.DEFAULT]: this.defaultQueue, + [QueueName.NOTIFICATIONS]: this.notificationsQueue, + [QueueName.BLOCKCHAIN]: this.blockchainQueue, + [QueueName.ANALYTICS]: this.analyticsQueue, + }; + return instances[name]; + } + + private async countEntity(table: string): Promise { + try { + const result = await this.dataSource.query( + `SELECT COUNT(*) as count FROM ${table}`, + ); + return Number(result[0]?.count ?? 0); + } catch { + return 0; + } + } + + private getUptime(): number { + return Date.now() - this.startTime; + } +} diff --git a/src/admin/protocol/tests/protocol-admin.service.spec.ts b/src/admin/protocol/tests/protocol-admin.service.spec.ts new file mode 100644 index 00000000..a821ba2a --- /dev/null +++ b/src/admin/protocol/tests/protocol-admin.service.spec.ts @@ -0,0 +1,627 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { getQueueToken } from '@nestjs/bullmq'; +import { NotFoundException, ConflictException } from '@nestjs/common'; +import { Repository, DataSource } from 'typeorm'; +import { Admin, AdminRole } from '../../entities/admin.entity'; +import { AuditLog } from '../../../audit/entities/audit-log.entity'; +import { ProtocolAdminService } from '../protocol-admin.service'; +import { AuditTrailService } from '../../../audit/services/audit-trail.service'; +import { FeatureFlagsService } from '../../../feature-flags/feature-flags.service'; +import { ConfigurationService } from '../../../feature-flags/configuration.service'; +import { JobsService } from '../../../jobs/jobs.service'; +import { RedisService } from '../../../redis/redis.service'; +import { QueueName } from '../../../jobs/jobs.types'; +import { + ServiceType, + QueueAction, + ServiceAction, +} from '../dto/service-control.dto'; +import { + EmergencyAction, + ExecuteEmergencyActionDto, +} from '../dto/emergency.dto'; +import { SetMaintenanceModeDto } from '../dto/maintenance.dto'; + +describe('ProtocolAdminService', () => { + let service: ProtocolAdminService; + + const mockAdminRepo = { + findOne: jest.fn(), + findOneBy: jest.fn(), + create: jest.fn(), + save: jest.fn(), + findAndCount: jest.fn(), + count: jest.fn(), + }; + + const mockAuditLogRepo = { + find: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + findAndCount: jest.fn(), + count: jest.fn().mockResolvedValue(42), + }; + + const mockDataSource = { + query: jest.fn().mockResolvedValue([{ count: 100 }]), + }; + + const mockAuditTrailService = { + log: jest.fn().mockResolvedValue(undefined), + }; + + const mockFeatureFlagsService = { + findAll: jest.fn().mockResolvedValue([]), + evaluate: jest.fn().mockResolvedValue({ enabled: true }), + }; + + const mockConfigService = { + get: jest.fn().mockResolvedValue(null), + set: jest.fn().mockResolvedValue({ id: 'cfg-1', key: 'test-key' }), + findAll: jest.fn().mockResolvedValue([]), + findOne: jest.fn().mockResolvedValue({ id: 'cfg-1' }), + delete: jest.fn().mockResolvedValue(undefined), + }; + + const mockJobsService = { + getAllQueueMetrics: jest.fn().mockResolvedValue([ + { + name: QueueName.DEFAULT, + waiting: 5, + active: 2, + completed: 100, + failed: 3, + delayed: 1, + paused: false, + }, + { + name: QueueName.NOTIFICATIONS, + waiting: 0, + active: 0, + completed: 50, + failed: 1, + delayed: 0, + paused: false, + }, + ]), + pauseQueue: jest.fn().mockResolvedValue(undefined), + resumeQueue: jest.fn().mockResolvedValue(undefined), + retryFailed: jest.fn().mockResolvedValue(3), + }; + + const mockRedisService = { + flushall: jest.fn().mockResolvedValue(true), + }; + + const mockDefaultQueue = { drain: jest.fn().mockResolvedValue(undefined) }; + const mockNotificationsQueue = { + drain: jest.fn().mockResolvedValue(undefined), + }; + const mockBlockchainQueue = { + drain: jest.fn().mockResolvedValue(undefined), + }; + const mockAnalyticsQueue = { drain: jest.fn().mockResolvedValue(undefined) }; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ProtocolAdminService, + { provide: getRepositoryToken(Admin), useValue: mockAdminRepo }, + { provide: getRepositoryToken(AuditLog), useValue: mockAuditLogRepo }, + { provide: DataSource, useValue: mockDataSource }, + { provide: AuditTrailService, useValue: mockAuditTrailService }, + { provide: FeatureFlagsService, useValue: mockFeatureFlagsService }, + { provide: ConfigurationService, useValue: mockConfigService }, + { provide: JobsService, useValue: mockJobsService }, + { provide: RedisService, useValue: mockRedisService }, + { provide: getQueueToken(QueueName.DEFAULT), useValue: mockDefaultQueue }, + { + provide: getQueueToken(QueueName.NOTIFICATIONS), + useValue: mockNotificationsQueue, + }, + { + provide: getQueueToken(QueueName.BLOCKCHAIN), + useValue: mockBlockchainQueue, + }, + { + provide: getQueueToken(QueueName.ANALYTICS), + useValue: mockAnalyticsQueue, + }, + ], + }).compile(); + + service = module.get(ProtocolAdminService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + // ────────────────────────────────────────────── + // SYSTEM STATUS TESTS + // ────────────────────────────────────────────── + + describe('getSystemStatus', () => { + it('should return system status with defaults', async () => { + const status = await service.getSystemStatus(); + + expect(status.maintenanceMode).toBe(false); + expect(status.emergencyActive).toBe(false); + expect(status.activeEmergencies).toEqual([]); + expect(status.queuesOperational).toBe(true); + expect(status.notificationsEnabled).toBe(true); + expect(status.integrationsOperational).toBe(true); + expect(status.environment).toBeDefined(); + expect(status.uptime).toBeGreaterThanOrEqual(0); + }); + }); + + describe('getOperationalStats', () => { + it('should return aggregated operational statistics', async () => { + const stats = await service.getOperationalStats(); + + expect(stats).toHaveProperty('totalUsers'); + expect(stats).toHaveProperty('totalAdmins'); + expect(stats).toHaveProperty('auditLogCount'); + expect(stats.queueMetrics).toHaveProperty('totalWaiting'); + expect(stats.queueMetrics).toHaveProperty('totalActive'); + expect(stats.queueMetrics).toHaveProperty('totalFailed'); + expect(stats.queueMetrics).toHaveProperty('totalCompleted'); + expect(stats.environment).toBeDefined(); + expect(stats.timestamp).toBeDefined(); + }); + + it('should query database for user and admin counts', async () => { + await service.getOperationalStats(); + + expect(mockDataSource.query).toHaveBeenCalled(); + expect(mockAdminRepo.count).toHaveBeenCalled(); + expect(mockAuditLogRepo.count).toHaveBeenCalled(); + }); + }); + + // ────────────────────────────────────────────── + // MAINTENANCE MODE TESTS + // ────────────────────────────────────────────── + + describe('setMaintenanceMode', () => { + it('should enable maintenance mode', async () => { + const dto: SetMaintenanceModeDto = { + enabled: true, + reason: 'Scheduled upgrade', + }; + + const result = await service.setMaintenanceMode(dto, 'admin-1'); + + expect(result.active).toBe(true); + expect(result.reason).toBe('Scheduled upgrade'); + expect(result.startedAt).toBeDefined(); + expect(mockAuditTrailService.log).toHaveBeenCalledWith( + expect.objectContaining({ + actionType: 'MAINTENANCE_MODE_ENABLED', + description: expect.stringContaining('Scheduled upgrade'), + }), + ); + }); + + it('should disable maintenance mode', async () => { + // Enable first + await service.setMaintenanceMode( + { enabled: true, reason: 'test' }, + 'admin-1', + ); + + const dto: SetMaintenanceModeDto = { enabled: false }; + const result = await service.setMaintenanceMode(dto, 'admin-1'); + + expect(result.active).toBe(false); + expect(mockAuditTrailService.log).toHaveBeenCalledWith( + expect.objectContaining({ + actionType: 'MAINTENANCE_MODE_DISABLED', + }), + ); + }); + + it('should set scheduled end time', async () => { + const dto: SetMaintenanceModeDto = { + enabled: true, + scheduledEnd: '2026-08-01T00:00:00Z', + }; + + const result = await service.setMaintenanceMode(dto, 'admin-1'); + + expect(result.scheduledEnd).toBe('2026-08-01T00:00:00Z'); + }); + }); + + describe('scheduleMaintenance', () => { + it('should create a maintenance schedule', async () => { + const schedule = await service.scheduleMaintenance( + { + startTime: '2026-08-01T02:00:00Z', + description: 'Database upgrade', + affectedServices: ['database', 'api'], + }, + 'admin-1', + ); + + expect(schedule.id).toBeDefined(); + expect(schedule.description).toBe('Database upgrade'); + expect(schedule.status).toBe('scheduled'); + expect(schedule.affectedServices).toEqual(['database', 'api']); + expect(mockAuditTrailService.log).toHaveBeenCalled(); + }); + }); + + describe('cancelMaintenance', () => { + it('should cancel a scheduled maintenance', async () => { + const schedule = await service.scheduleMaintenance( + { + startTime: '2026-08-01T02:00:00Z', + description: 'Database upgrade', + }, + 'admin-1', + ); + + await service.cancelMaintenance(schedule.id, 'admin-1'); + + const status = await service.getMaintenanceStatus(); + expect( + status.scheduledMaintenance.find((s) => s.id === schedule.id), + ).toBeUndefined(); + }); + + it('should throw NotFoundException for unknown schedule', async () => { + await expect( + service.cancelMaintenance('nonexistent-id', 'admin-1'), + ).rejects.toThrow(NotFoundException); + }); + }); + + // ────────────────────────────────────────────── + // SERVICE MANAGEMENT TESTS + // ────────────────────────────────────────────── + + describe('controlService', () => { + it('should pause a queue', async () => { + const result = await service.controlService( + { + serviceType: ServiceType.QUEUE, + action: QueueAction.PAUSE, + queueName: QueueName.DEFAULT, + }, + 'admin-1', + ); + + expect(result.success).toBe(true); + expect(result.action).toBe(QueueAction.PAUSE); + expect(result.serviceType).toBe(ServiceType.QUEUE); + expect(mockJobsService.pauseQueue).toHaveBeenCalledWith( + QueueName.DEFAULT, + ); + expect(mockAuditTrailService.log).toHaveBeenCalled(); + }); + + it('should resume a queue', async () => { + const result = await service.controlService( + { + serviceType: ServiceType.QUEUE, + action: QueueAction.RESUME, + }, + 'admin-1', + ); + + expect(result.success).toBe(true); + expect(mockJobsService.resumeQueue).toHaveBeenCalled(); + }); + + it('should retry failed jobs', async () => { + const result = await service.controlService( + { + serviceType: ServiceType.QUEUE, + action: QueueAction.RETRY_FAILED, + }, + 'admin-1', + ); + + expect(result.success).toBe(true); + expect(mockJobsService.retryFailed).toHaveBeenCalled(); + }); + + it('should suspend notification service', async () => { + const result = await service.controlService( + { + serviceType: ServiceType.NOTIFICATION, + action: ServiceAction.SUSPEND, + reason: 'Testing', + }, + 'admin-1', + ); + + expect(result.success).toBe(true); + expect(mockAuditTrailService.log).toHaveBeenCalledWith( + expect.objectContaining({ + actionType: 'SERVICE_SUSPENDED', + description: expect.stringContaining('Testing'), + }), + ); + }); + + it('should restore notification service', async () => { + const result = await service.controlService( + { + serviceType: ServiceType.NOTIFICATION, + action: ServiceAction.RESTORE, + }, + 'admin-1', + ); + + expect(result.success).toBe(true); + }); + + it('should invalidate cache', async () => { + const result = await service.controlService( + { + serviceType: ServiceType.CACHE, + action: ServiceAction.INVALIDATE_CACHE, + }, + 'admin-1', + ); + + expect(result.success).toBe(true); + expect(mockRedisService.flushall).toHaveBeenCalled(); + expect(mockAuditTrailService.log).toHaveBeenCalledWith( + expect.objectContaining({ + actionType: 'CACHE_INVALIDATED', + }), + ); + }); + + it('should return error on unknown service type', async () => { + const result = await service.controlService( + { + serviceType: 'unknown' as ServiceType, + action: QueueAction.PAUSE, + }, + 'admin-1', + ); + + expect(result.success).toBe(false); + expect(result.message).toBeDefined(); + }); + }); + + describe('getQueueMetrics', () => { + it('should return metrics for all queues', async () => { + const result = await service.getQueueMetrics(); + + expect(result.queues).toHaveLength(2); + expect(result.totalWaiting).toBe(5); + expect(result.totalActive).toBe(2); + expect(result.totalFailed).toBe(4); + }); + }); + + // ────────────────────────────────────────────── + // EMERGENCY OPERATIONS TESTS + // ────────────────────────────────────────────── + + describe('executeEmergencyAction', () => { + it('should execute an emergency action', async () => { + const dto: ExecuteEmergencyActionDto = { + action: EmergencyAction.PAUSE_ALL_QUEUES, + reason: 'Critical incident', + }; + + const result = await service.executeEmergencyAction(dto, 'admin-1'); + + expect(result.success).toBe(true); + expect(result.action).toBe(EmergencyAction.PAUSE_ALL_QUEUES); + expect(result.affectedServices).toContain('queues'); + expect(mockAuditTrailService.log).toHaveBeenCalledWith( + expect.objectContaining({ + actionType: 'EMERGENCY_ACTION_EXECUTED', + severity: 'CRITICAL', + description: expect.stringContaining('Critical incident'), + }), + ); + }); + + it('should execute action with optional duration', async () => { + const dto: ExecuteEmergencyActionDto = { + action: EmergencyAction.DISABLE_NOTIFICATIONS, + reason: 'Rate limit exceeded', + durationMinutes: 30, + }; + + const result = await service.executeEmergencyAction(dto, 'admin-1'); + + expect(result.success).toBe(true); + }); + + it('should reflect emergency in system status', async () => { + await service.executeEmergencyAction( + { + action: EmergencyAction.SUSPEND_ALL_SERVICES, + reason: 'Emergency test', + }, + 'admin-1', + ); + + const status = await service.getSystemStatus(); + expect(status.emergencyActive).toBe(true); + expect(status.activeEmergencies).toContain( + EmergencyAction.SUSPEND_ALL_SERVICES, + ); + }); + }); + + describe('resolveEmergencyAction', () => { + it('should resolve an active emergency action', async () => { + await service.executeEmergencyAction( + { + action: EmergencyAction.DISABLE_NOTIFICATIONS, + reason: 'test', + }, + 'admin-1', + ); + + await service.resolveEmergencyAction( + EmergencyAction.DISABLE_NOTIFICATIONS, + 'admin-1', + ); + + const status = await service.getSystemStatus(); + expect(status.activeEmergencies).not.toContain( + EmergencyAction.DISABLE_NOTIFICATIONS, + ); + expect(mockAuditTrailService.log).toHaveBeenCalledWith( + expect.objectContaining({ + actionType: 'SERVICE_RESTORED', + }), + ); + }); + + it('should throw NotFoundException for inactive emergency', async () => { + await expect( + service.resolveEmergencyAction( + EmergencyAction.EMERGENCY_SHUTDOWN, + 'admin-1', + ), + ).rejects.toThrow(NotFoundException); + }); + }); + + // ────────────────────────────────────────────── + // CONFIGURATION MANAGEMENT TESTS + // ────────────────────────────────────────────── + + describe('setProtocolConfig', () => { + it('should set a protocol configuration value', async () => { + const result = await service.setProtocolConfig( + { + key: 'test-key', + value: { setting: 'value' }, + changeReason: 'Test change', + }, + 'admin-1', + ); + + expect(result).toBeDefined(); + expect(mockConfigService.set).toHaveBeenCalledWith( + 'test-key', + { setting: 'value' }, + undefined, + 'admin-1', + 'Test change', + ); + expect(mockAuditTrailService.log).toHaveBeenCalledWith( + expect.objectContaining({ + actionType: 'PROTOCOL_CONFIG_UPDATED', + description: expect.stringContaining('Test change'), + }), + ); + }); + }); + + describe('getProtocolConfig', () => { + it('should get a protocol configuration value', async () => { + mockConfigService.get.mockResolvedValue({ setting: 'value' }); + + const result = await service.getProtocolConfig('test-key'); + + expect(result).toEqual({ setting: 'value' }); + }); + }); + + // ────────────────────────────────────────────── + // AUDIT LOG TESTS + // ────────────────────────────────────────────── + + describe('getAdminAuditLogs', () => { + it('should return admin audit logs', async () => { + mockAuditLogRepo.findAndCount.mockResolvedValue([ + [{ id: 'log-1' }], + 1, + ]); + + const result = await service.getAdminAuditLogs(10, 0); + + expect(result.logs).toHaveLength(1); + expect(result.total).toBe(1); + }); + }); + + describe('getProtocolAuditLogs', () => { + it('should return protocol audit logs', async () => { + mockAuditLogRepo.findAndCount.mockResolvedValue([ + [{ id: 'log-1' }, { id: 'log-2' }], + 2, + ]); + + const result = await service.getProtocolAuditLogs(20, 0); + + expect(result.logs).toHaveLength(2); + }); + }); + + // ────────────────────────────────────────────── + // MAINTENANCE STATUS TESTS + // ────────────────────────────────────────────── + + describe('getMaintenanceStatus', () => { + it('should return empty maintenance when not active', async () => { + const status = await service.getMaintenanceStatus(); + + expect(status.active).toBe(false); + expect(status.scheduledMaintenance).toEqual([]); + }); + + it('should include scheduled maintenance', async () => { + await service.scheduleMaintenance( + { + startTime: '2026-08-01T02:00:00Z', + description: 'Scheduled maintenance', + }, + 'admin-1', + ); + + const status = await service.getMaintenanceStatus(); + expect(status.scheduledMaintenance).toHaveLength(1); + expect(status.scheduledMaintenance[0].description).toBe( + 'Scheduled maintenance', + ); + }); + }); + + // ────────────────────────────────────────────── + // FEATURE FLAGS + // ────────────────────────────────────────────── + + describe('listFeatureFlags', () => { + it('should list feature flags', async () => { + const result = await service.listFeatureFlags(); + + expect(mockFeatureFlagsService.findAll).toHaveBeenCalled(); + expect(result).toEqual([]); + }); + }); + + describe('evaluateFeatureFlag', () => { + it('should evaluate a feature flag', async () => { + const result = await service.evaluateFeatureFlag('test-flag', { + userId: 'user-1', + }); + + expect(mockFeatureFlagsService.evaluate).toHaveBeenCalledWith( + 'test-flag', + { userId: 'user-1' }, + ); + expect(result).toEqual({ enabled: true }); + }); + }); +}); diff --git a/src/audit/entities/audit-log.entity.ts b/src/audit/entities/audit-log.entity.ts index 6fd52cae..6f22eed1 100644 --- a/src/audit/entities/audit-log.entity.ts +++ b/src/audit/entities/audit-log.entity.ts @@ -57,6 +57,23 @@ export enum AuditActionType { LEGAL_HOLD_REMOVED = 'LEGAL_HOLD_REMOVED', RETENTION_EXECUTED = 'RETENTION_EXECUTED', ARCHIVAL_COMPLETED = 'ARCHIVAL_COMPLETED', + // Protocol Administration actions + MAINTENANCE_MODE_ENABLED = 'MAINTENANCE_MODE_ENABLED', + MAINTENANCE_MODE_DISABLED = 'MAINTENANCE_MODE_DISABLED', + QUEUE_PAUSED = 'QUEUE_PAUSED', + QUEUE_RESUMED = 'QUEUE_RESUMED', + SERVICE_SUSPENDED = 'SERVICE_SUSPENDED', + SERVICE_RESTORED = 'SERVICE_RESTORED', + CACHE_INVALIDATED = 'CACHE_INVALIDATED', + INTEGRATION_SUSPENDED = 'INTEGRATION_SUSPENDED', + INTEGRATION_RESTORED = 'INTEGRATION_RESTORED', + API_THROTTLING_CONFIGURED = 'API_THROTTLING_CONFIGURED', + MAINTENANCE_SCHEDULED = 'MAINTENANCE_SCHEDULED', + MAINTENANCE_CANCELLED = 'MAINTENANCE_CANCELLED', + PROTOCOL_CONFIG_UPDATED = 'PROTOCOL_CONFIG_UPDATED', + EMERGENCY_ACTION_EXECUTED = 'EMERGENCY_ACTION_EXECUTED', + SERVICE_HEALTH_CHECK = 'SERVICE_HEALTH_CHECK', + METRICS_EXPORTED = 'METRICS_EXPORTED', } export enum AuditEntityType { @@ -77,6 +94,12 @@ export enum AuditEntityType { ROLE = 'ROLE', AUDIT_LOG = 'AUDIT_LOG', REPORT = 'REPORT', + MAINTENANCE = 'MAINTENANCE', + INTEGRATION = 'INTEGRATION', + SERVICE = 'SERVICE', + QUEUE = 'QUEUE', + CACHE = 'CACHE', + METRICS = 'METRICS', } export enum AuditSeverity { @@ -100,6 +123,10 @@ export enum AuditCategory { COMPLIANCE = 'COMPLIANCE', DATA_MANAGEMENT = 'DATA_MANAGEMENT', OPERATIONS = 'OPERATIONS', + MAINTENANCE = 'MAINTENANCE', + EMERGENCY = 'EMERGENCY', + SERVICE_CONTROL = 'SERVICE_CONTROL', + PROTOCOL = 'PROTOCOL', } @Entity('audit_logs') diff --git a/src/feature-flags/configuration.service.ts b/src/feature-flags/configuration.service.ts index ec6297b9..ad49b1c9 100644 --- a/src/feature-flags/configuration.service.ts +++ b/src/feature-flags/configuration.service.ts @@ -67,7 +67,7 @@ export class ConfigurationService { if (existing) { const nextVersion = existing.version + 1; await this.configRepo.update(existing.id, { - value: value as unknown, + value: value as any, version: nextVersion, createdBy, changeReason, @@ -78,7 +78,7 @@ export class ConfigurationService { const record = this.configRepo.create({ key, - value: value as unknown, + value: value as any, environment: env, version: 1, createdBy, diff --git a/src/redis/redis.service.ts b/src/redis/redis.service.ts index e7921a18..693bce06 100644 --- a/src/redis/redis.service.ts +++ b/src/redis/redis.service.ts @@ -171,6 +171,25 @@ export class RedisService implements OnModuleInit, OnModuleDestroy { } } + /** + * Flush all Redis data (use with extreme caution) + */ + async flushall(): Promise { + if (!this.client || !this.isConnected) { + this.logger.warn('Redis unavailable, skipping FLUSHALL'); + return false; + } + + try { + await this.client.flushall(); + this.logger.warn('Redis flushed all data'); + return true; + } catch (error) { + this.logger.error(`Redis FLUSHALL error: ${error.message}`); + return false; + } + } + /** * Get Redis connection status */