feat: Goa GEL Blockchain e-Licensing Platform - Full Stack Implementation

Complete implementation of the Goa Government e-Licensing platform with:

Backend:
- NestJS API with JWT authentication
- PostgreSQL database with Knex ORM
- Redis caching and session management
- MinIO document storage
- Hyperledger Besu blockchain integration
- Multi-department workflow system
- Comprehensive API tests (266/282 passing)

Frontend:
- Angular 21 with standalone components
- Angular Material + TailwindCSS UI
- Visual workflow builder
- Document upload with progress tracking
- Blockchain explorer integration
- Role-based dashboards (Admin, Department, Citizen)
- E2E tests with Playwright (37 tests)

Infrastructure:
- Docker Compose orchestration
- Blockscout blockchain explorer
- Development and production configurations
This commit is contained in:
Mahi
2026-02-07 10:23:29 -04:00
commit 80566bf0a2
441 changed files with 102418 additions and 0 deletions

View File

@@ -0,0 +1,336 @@
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,
},
}));
}
}