import { Injectable, Logger, Inject } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ethers } from 'ethers'; import { LicenseRequest } from '../../database/models/license-request.model'; import { Applicant } from '../../database/models/applicant.model'; import { Department } from '../../database/models/department.model'; import { User } from '../../database/models/user.model'; import { Document } from '../../database/models/document.model'; import { BlockchainTransaction } from '../../database/models/blockchain-transaction.model'; import { BlockchainEvent } from '../../database/models/blockchain-event.model'; import { ApplicationLog } from '../../database/models/application-log.model'; import { AuditLog } from '../../database/models/audit-log.model'; import { DepartmentsService } from '../departments/departments.service'; import { UsersService } from '../users/users.service'; export interface StatusCount { status: string; count: number; } export interface PlatformStats { totalRequests: number; totalApprovals: number; requestsByStatus: StatusCount[]; totalApplicants: number; activeApplicants: number; totalDepartments: number; activeDepartments: number; totalDocuments: number; totalBlockchainTransactions: number; transactionsByStatus: StatusCount[]; averageProcessingTime: number; lastUpdated: string; } export interface SystemHealth { status: 'healthy' | 'degraded' | 'unhealthy'; uptime: number; timestamp: string; services: { database: { status: string }; blockchain: { status: string }; storage: { status: string }; queue: { status: string }; }; } @Injectable() export class AdminService { private readonly logger = new Logger(AdminService.name); constructor( @Inject(LicenseRequest) private requestModel: typeof LicenseRequest, @Inject(Applicant) private applicantModel: typeof Applicant, @Inject(Department) private departmentModel: typeof Department, @Inject(User) private userModel: typeof User, @Inject(Document) private documentModel: typeof Document, @Inject(BlockchainTransaction) private blockchainTxModel: typeof BlockchainTransaction, @Inject(BlockchainEvent) private blockchainEventModel: typeof BlockchainEvent, @Inject(ApplicationLog) private appLogModel: typeof ApplicationLog, @Inject(AuditLog) private auditLogModel: typeof AuditLog, private readonly configService: ConfigService, private readonly departmentsService: DepartmentsService, private readonly usersService: UsersService, ) {} async getPlatformStats(): Promise { this.logger.debug('Fetching platform statistics'); const [ totalRequests, requestsByStatus, totalApplicants, activeApplicants, totalDepartments, activeDepartments, totalDocuments, totalBlockchainTransactions, transactionsByStatus, ] = await Promise.all([ this.requestModel.query().resultSize(), this.requestModel.query().select('status').count('* as count').groupBy('status') as any, this.applicantModel.query().resultSize(), this.applicantModel.query().where({ is_active: true }).resultSize(), this.departmentModel.query().resultSize(), this.departmentModel.query().where({ is_active: true }).resultSize(), this.documentModel.query().resultSize(), this.blockchainTxModel.query().resultSize(), this.blockchainTxModel.query().select('status').count('* as count').groupBy('status') as any, ]); // Convert to array format expected by frontend const statusArray: StatusCount[] = requestsByStatus.map((row: any) => ({ status: row.status, count: parseInt(row.count, 10), })); const txStatusArray: StatusCount[] = transactionsByStatus.map((row: any) => ({ status: row.status, count: parseInt(row.count, 10), })); // Calculate total approvals const approvedCount = statusArray.find(s => s.status === 'APPROVED')?.count || 0; return { totalRequests, totalApprovals: approvedCount, requestsByStatus: statusArray, totalApplicants, activeApplicants, totalDepartments, activeDepartments, totalDocuments, totalBlockchainTransactions, transactionsByStatus: txStatusArray, averageProcessingTime: 4.5, // Placeholder lastUpdated: new Date().toISOString(), }; } async getSystemHealth(): Promise { this.logger.debug('Checking system health'); let dbStatus = 'up'; try { await this.applicantModel.query().limit(1); } catch { dbStatus = 'down'; } const overallStatus = dbStatus === 'up' ? 'healthy' : 'unhealthy'; return { status: overallStatus, uptime: process.uptime(), timestamp: new Date().toISOString(), services: { database: { status: dbStatus }, blockchain: { status: 'up' }, storage: { status: 'up' }, queue: { status: 'up' }, }, }; } async getRecentActivity(limit: number = 20): Promise { return this.auditLogModel.query().orderBy('created_at', 'DESC').limit(limit); } async getBlockchainTransactions(page: number = 1, limit: number = 20, status?: string) { const query = this.blockchainTxModel.query().orderBy('created_at', 'DESC'); if (status) { query.where({ status }); } const offset = (page - 1) * limit; const [results, total] = await Promise.all([ query.clone().offset(offset).limit(limit), query.clone().resultSize(), ]); return { data: results, total, page, limit, totalPages: Math.ceil(total / limit), }; } async onboardDepartment(dto: any) { this.logger.debug(`Onboarding new department: ${dto.code}`); const result = await this.departmentsService.create(dto); return { department: result.department, apiKey: result.apiKey, apiSecret: result.apiSecret, message: 'Department onboarded successfully. Please save the API credentials as they will not be shown again.', }; } async getDepartments(page: number = 1, limit: number = 20) { return this.departmentsService.findAll({ page, limit }); } async getDepartment(id: string) { return this.departmentsService.findById(id); } async updateDepartment(id: string, dto: any) { return this.departmentsService.update(id, dto); } async regenerateDepartmentApiKey(id: string) { const result = await this.departmentsService.regenerateApiKey(id); return { ...result, message: 'API key regenerated successfully. Please save the new credentials as they will not be shown again.', }; } async deactivateDepartment(id: string) { await this.departmentsService.deactivate(id); return { message: 'Department deactivated successfully' }; } async activateDepartment(id: string) { const department = await this.departmentsService.activate(id); return { department, message: 'Department activated successfully' }; } async getUsers() { return this.usersService.findAll(); } async getBlockchainEvents( page: number = 1, limit: number = 20, eventType?: string, contractAddress?: string, ) { const query = this.blockchainEventModel.query().orderBy('created_at', 'DESC'); if (eventType) { query.where({ eventType }); } if (contractAddress) { query.where({ contractAddress }); } const offset = (page - 1) * limit; const [results, total] = await Promise.all([ query.clone().offset(offset).limit(limit), query.clone().resultSize(), ]); return { data: results, total, page, limit, totalPages: Math.ceil(total / limit), }; } async getApplicationLogs( page: number = 1, limit: number = 50, level?: string, module?: string, search?: string, ) { const query = this.appLogModel.query().orderBy('created_at', 'DESC'); if (level) { query.where({ level }); } if (module) { query.where('module', 'like', `%${module}%`); } if (search) { query.where('message', 'like', `%${search}%`); } const offset = (page - 1) * limit; const [results, total] = await Promise.all([ query.clone().offset(offset).limit(limit), query.clone().resultSize(), ]); return { data: results, total, page, limit, totalPages: Math.ceil(total / limit), }; } async getRequestDocuments(requestId: string) { this.logger.debug(`Fetching documents for request: ${requestId}`); // Fetch all documents for the request with related data const documents = await this.documentModel .query() .where({ requestId }) .withGraphFetched( '[uploadedByUser, versions.[uploadedByUser], departmentReviews.[department]]', ) .orderBy('created_at', 'DESC'); // Transform documents to include formatted data return documents.map((doc: any) => ({ id: doc.id, name: doc.name, type: doc.type, size: doc.size, fileHash: doc.fileHash, ipfsHash: doc.ipfsHash, url: doc.url, thumbnailUrl: doc.thumbnailUrl, uploadedAt: doc.createdAt, uploadedBy: doc.uploadedByUser?.name || 'Unknown', currentVersion: doc.version || 1, versions: doc.versions?.map((v: any) => ({ id: v.id, version: v.version, fileHash: v.fileHash, uploadedAt: v.createdAt, uploadedBy: v.uploadedByUser?.name || 'Unknown', changes: v.changes, })) || [], departmentReviews: doc.departmentReviews?.map((review: any) => ({ departmentCode: review.department?.code || 'UNKNOWN', departmentName: review.department?.name || 'Unknown Department', reviewedAt: review.createdAt, reviewedBy: review.reviewedByUser?.name || 'Unknown', status: review.status, comments: review.comments, })) || [], metadata: { mimeType: doc.mimeType, width: doc.width, height: doc.height, pages: doc.pages, }, })); } async getBlockchainBlocks(limit: number = 5) { this.logger.debug(`Fetching ${limit} recent blockchain blocks`); try { const rpcUrl = this.configService.get('BESU_RPC_URL') || 'http://besu-node-1:8545'; const provider = new ethers.JsonRpcProvider(rpcUrl); const latestBlockNumber = await provider.getBlockNumber(); const blocks = []; for (let i = 0; i < limit && latestBlockNumber - i >= 0; i++) { const blockNumber = latestBlockNumber - i; const block = await provider.getBlock(blockNumber); if (block) { blocks.push({ blockNumber: block.number, hash: block.hash, parentHash: block.parentHash, timestamp: new Date(block.timestamp * 1000).toISOString(), transactionCount: block.transactions.length, gasUsed: Number(block.gasUsed), gasLimit: Number(block.gasLimit), miner: block.miner, nonce: block.nonce, }); } } return { data: blocks }; } catch (error) { this.logger.error('Failed to fetch blockchain blocks', error); // Return empty array on error - frontend will use mock data return { data: [] }; } } }