337 lines
9.7 KiB
TypeScript
337 lines
9.7 KiB
TypeScript
|
|
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||
|
|
import { ConfigService } from '@nestjs/config';
|
||
|
|
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 PlatformStats {
|
||
|
|
totalRequests: number;
|
||
|
|
requestsByStatus: Record<string, number>;
|
||
|
|
totalApplicants: number;
|
||
|
|
activeApplicants: number;
|
||
|
|
totalDepartments: number;
|
||
|
|
activeDepartments: number;
|
||
|
|
totalDocuments: number;
|
||
|
|
totalBlockchainTransactions: number;
|
||
|
|
transactionsByStatus: Record<string, number>;
|
||
|
|
}
|
||
|
|
|
||
|
|
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<PlatformStats> {
|
||
|
|
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({ isActive: true }).resultSize(),
|
||
|
|
this.departmentModel.query().resultSize(),
|
||
|
|
this.departmentModel.query().where({ isActive: true }).resultSize(),
|
||
|
|
this.documentModel.query().resultSize(),
|
||
|
|
this.blockchainTxModel.query().resultSize(),
|
||
|
|
this.blockchainTxModel
|
||
|
|
.query()
|
||
|
|
.select('status')
|
||
|
|
.count('* as count')
|
||
|
|
.groupBy('status') as any,
|
||
|
|
]);
|
||
|
|
|
||
|
|
const statusMap: Record<string, number> = {};
|
||
|
|
for (const row of requestsByStatus) {
|
||
|
|
statusMap[(row as any).status] = parseInt((row as any).count, 10);
|
||
|
|
}
|
||
|
|
|
||
|
|
const txStatusMap: Record<string, number> = {};
|
||
|
|
for (const row of transactionsByStatus) {
|
||
|
|
txStatusMap[(row as any).status] = parseInt((row as any).count, 10);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
totalRequests,
|
||
|
|
requestsByStatus: statusMap,
|
||
|
|
totalApplicants,
|
||
|
|
activeApplicants,
|
||
|
|
totalDepartments,
|
||
|
|
activeDepartments,
|
||
|
|
totalDocuments,
|
||
|
|
totalBlockchainTransactions,
|
||
|
|
transactionsByStatus: txStatusMap,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async getSystemHealth(): Promise<SystemHealth> {
|
||
|
|
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<AuditLog[]> {
|
||
|
|
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,
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}
|