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,3 @@
<claude-mem-context>
</claude-mem-context>

View File

@@ -0,0 +1,90 @@
import {
Controller,
Get,
Query,
Param,
UseGuards,
Logger,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { AuditService, AuditQueryDto } from './audit.service';
import { Roles } from '../../common/decorators/roles.decorator';
import { RolesGuard } from '../../common/guards/roles.guard';
import { UserRole } from '../../common/enums';
@ApiTags('Audit')
@Controller('audit')
@ApiBearerAuth('BearerAuth')
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.ADMIN)
export class AuditController {
private readonly logger = new Logger(AuditController.name);
constructor(private readonly auditService: AuditService) {}
@Get('logs')
@ApiOperation({
summary: 'Query audit logs',
description: 'Get paginated audit logs with optional filters by entity, action, actor, and date range',
})
@ApiQuery({ name: 'entityType', required: false, description: 'Filter by entity type (REQUEST, DOCUMENT, DEPARTMENT, etc.)' })
@ApiQuery({ name: 'entityId', required: false, description: 'Filter by entity ID' })
@ApiQuery({ name: 'action', required: false, description: 'Filter by action (CREATE, UPDATE, DELETE, APPROVE, REJECT, etc.)' })
@ApiQuery({ name: 'actorType', required: false, description: 'Filter by actor type (APPLICANT, DEPARTMENT, SYSTEM, ADMIN)' })
@ApiQuery({ name: 'actorId', required: false, description: 'Filter by actor ID' })
@ApiQuery({ name: 'startDate', required: false, description: 'Filter from date (ISO 8601)' })
@ApiQuery({ name: 'endDate', required: false, description: 'Filter to date (ISO 8601)' })
@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)' })
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default: 20)' })
@ApiResponse({ status: 200, description: 'Paginated audit logs' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden - Admin only' })
async findAll(@Query() query: AuditQueryDto) {
this.logger.debug('Querying audit logs');
return this.auditService.findAll(query);
}
@Get('requests/:requestId')
@ApiOperation({
summary: 'Get audit trail for request',
description: 'Get complete audit trail for a specific license request',
})
@ApiParam({ name: 'requestId', description: 'Request ID (UUID)' })
@ApiResponse({ status: 200, description: 'Audit trail for request' })
async findByRequest(@Param('requestId') requestId: string) {
return this.auditService.findByEntity('REQUEST', requestId);
}
@Get('entity/:entityType/:entityId')
@ApiOperation({
summary: 'Get audit trail for entity',
description: 'Get complete audit trail for a specific entity (e.g., all changes to a request)',
})
@ApiParam({ name: 'entityType', description: 'Entity type (REQUEST, DOCUMENT, DEPARTMENT, etc.)' })
@ApiParam({ name: 'entityId', description: 'Entity ID (UUID)' })
@ApiResponse({ status: 200, description: 'Audit trail for entity' })
async findByEntity(
@Param('entityType') entityType: string,
@Param('entityId') entityId: string,
) {
return this.auditService.findByEntity(entityType, entityId);
}
@Get('metadata')
@ApiOperation({
summary: 'Get audit metadata',
description: 'Get available audit actions, entity types, and actor types for filtering',
})
@ApiResponse({ status: 200, description: 'Audit metadata' })
async getMetadata() {
return this.auditService.getEntityActions();
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { AuditController } from './audit.controller';
import { AuditService } from './audit.service';
@Module({
controllers: [AuditController],
providers: [AuditService],
exports: [AuditService],
})
export class AuditModule {}

View File

@@ -0,0 +1,124 @@
import { Injectable, Logger, Inject } from '@nestjs/common';
import { AuditLog } from '../../database/models/audit-log.model';
import { AuditAction, ActorType, EntityType } from '../../common/enums';
export interface CreateAuditLogDto {
entityType: string;
entityId: string;
action: string;
actorType: string;
actorId?: string;
oldValue?: Record<string, unknown>;
newValue?: Record<string, unknown>;
ipAddress?: string;
userAgent?: string;
correlationId?: string;
}
export interface AuditQueryDto {
entityType?: string;
entityId?: string;
action?: string;
actorType?: string;
actorId?: string;
startDate?: string;
endDate?: string;
page?: number;
limit?: number;
}
@Injectable()
export class AuditService {
private readonly logger = new Logger(AuditService.name);
constructor(
@Inject(AuditLog)
private auditLogModel: typeof AuditLog,
) {}
async record(dto: CreateAuditLogDto): Promise<AuditLog> {
this.logger.debug(
`Recording audit: ${dto.action} on ${dto.entityType}/${dto.entityId}`,
);
return this.auditLogModel.query().insert({
entityType: dto.entityType,
entityId: dto.entityId,
action: dto.action,
actorType: dto.actorType,
actorId: dto.actorId,
oldValue: dto.oldValue as any,
newValue: dto.newValue as any,
ipAddress: dto.ipAddress,
userAgent: dto.userAgent,
correlationId: dto.correlationId,
});
}
async findAll(queryDto: AuditQueryDto) {
const page = queryDto.page || 1;
const limit = queryDto.limit || 20;
const offset = (page - 1) * limit;
const query = this.auditLogModel.query().orderBy('created_at', 'DESC');
if (queryDto.entityType) {
query.where({ entityType: queryDto.entityType });
}
if (queryDto.entityId) {
query.where({ entityId: queryDto.entityId });
}
if (queryDto.action) {
query.where({ action: queryDto.action });
}
if (queryDto.actorType) {
query.where({ actorType: queryDto.actorType });
}
if (queryDto.actorId) {
query.where({ actorId: queryDto.actorId });
}
if (queryDto.startDate) {
query.where('createdAt', '>=', queryDto.startDate);
}
if (queryDto.endDate) {
query.where('createdAt', '<=', queryDto.endDate);
}
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 findByEntity(entityType: string, entityId: string): Promise<any[]> {
const logs = await this.auditLogModel
.query()
.where('entity_type', entityType)
.where('entity_id', entityId)
.orderBy('created_at', 'DESC');
// Transform to add performedBy and details fields from newValue
return logs.map((log) => ({
...log,
performedBy: (log.newValue as any)?.performedBy,
details: (log.newValue as any)?.reason || (log.newValue as any)?.remarks ||
(log.action === 'REQUEST_CANCELLED' ? (log.newValue as any)?.reason : undefined),
}));
}
async getEntityActions(): Promise<{ actions: string[]; entityTypes: string[]; actorTypes: string[] }> {
return {
actions: Object.values(AuditAction),
entityTypes: Object.values(EntityType),
actorTypes: Object.values(ActorType),
};
}
}