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:
222
backend/src/modules/webhooks/services/webhooks.service.ts
Normal file
222
backend/src/modules/webhooks/services/webhooks.service.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Injectable, Logger, NotFoundException, BadRequestException, Inject } from '@nestjs/common';
|
||||
import { Webhook } from '../../../database/models/webhook.model';
|
||||
import { WebhookLog, WebhookLogStatus } from '../../../database/models/webhook-log.model';
|
||||
import { CreateWebhookDto, UpdateWebhookDto, WebhookTestResultDto } from '../dto';
|
||||
import { PaginationDto } from '../../../common/dto/pagination.dto';
|
||||
import * as crypto from 'crypto';
|
||||
import { paginate, PaginatedResult } from '../../../common/utils/pagination.util';
|
||||
|
||||
@Injectable()
|
||||
export class WebhooksService {
|
||||
private readonly logger = new Logger(WebhooksService.name);
|
||||
|
||||
constructor(
|
||||
@Inject(Webhook)
|
||||
private webhookRepository: typeof Webhook,
|
||||
@Inject(WebhookLog)
|
||||
private webhookLogRepository: typeof WebhookLog,
|
||||
) {}
|
||||
|
||||
async register(departmentId: string, dto: CreateWebhookDto): Promise<Webhook> {
|
||||
try {
|
||||
this.logger.debug(
|
||||
`Registering webhook for department: ${departmentId}, URL: ${dto.url}`,
|
||||
);
|
||||
|
||||
const secret = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
const saved = await this.webhookRepository.query().insertAndFetch({
|
||||
departmentId,
|
||||
url: dto.url,
|
||||
events: dto.events as any,
|
||||
secretHash: secret,
|
||||
isActive: true,
|
||||
});
|
||||
this.logger.log(`Webhook registered: ${saved.id}`);
|
||||
|
||||
return saved;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to register webhook', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(departmentId: string): Promise<Webhook[]> {
|
||||
try {
|
||||
return await this.webhookRepository.query()
|
||||
.where({ departmentId })
|
||||
.orderBy('created_at', 'DESC');
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to find webhooks for department ${departmentId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Webhook> {
|
||||
try {
|
||||
const webhook = await this.webhookRepository.query().findById(id);
|
||||
|
||||
if (!webhook) {
|
||||
throw new NotFoundException(`Webhook not found: ${id}`);
|
||||
}
|
||||
|
||||
return webhook;
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.error(`Failed to find webhook: ${id}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateWebhookDto): Promise<Webhook> {
|
||||
try {
|
||||
this.logger.debug(`Updating webhook: ${id}`);
|
||||
|
||||
const webhook = await this.findById(id);
|
||||
const updated = await webhook.$query().patchAndFetch(dto as any);
|
||||
this.logger.log(`Webhook updated: ${id}`);
|
||||
|
||||
return updated;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to update webhook: ${id}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
try {
|
||||
this.logger.debug(`Deleting webhook: ${id}`);
|
||||
|
||||
const deletedCount = await this.webhookRepository.query().deleteById(id);
|
||||
if (deletedCount === 0) {
|
||||
throw new NotFoundException(`Webhook not found: ${id}`);
|
||||
}
|
||||
|
||||
this.logger.log(`Webhook deleted: ${id}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to delete webhook: ${id}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async test(id: string): Promise<WebhookTestResultDto> {
|
||||
try {
|
||||
this.logger.debug(`Testing webhook: ${id}`);
|
||||
|
||||
const webhook = await this.findById(id);
|
||||
|
||||
const testPayload = {
|
||||
eventType: 'TEST_EVENT',
|
||||
timestamp: new Date(),
|
||||
data: { test: true },
|
||||
};
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const response = await fetch(webhook.url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Webhook-Signature': this.generateSignature(testPayload, webhook.secretHash),
|
||||
},
|
||||
body: JSON.stringify(testPayload),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
const result: WebhookTestResultDto = {
|
||||
success: response.ok,
|
||||
statusCode: response.status,
|
||||
statusMessage: response.statusText,
|
||||
responseTime,
|
||||
};
|
||||
|
||||
this.logger.log(`Webhook test successful: ${id}, status=${response.status}`);
|
||||
return result;
|
||||
} catch (fetchError) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
const error = fetchError as Error;
|
||||
|
||||
this.logger.warn(`Webhook test failed: ${id}, error=${error.message}`);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
statusCode: 0,
|
||||
statusMessage: 'Error',
|
||||
responseTime,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to test webhook: ${id}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getLogs(webhookId: string, pagination: PaginationDto): Promise<PaginatedResult<WebhookLog>> {
|
||||
try {
|
||||
await this.findById(webhookId);
|
||||
|
||||
const query = this.webhookLogRepository.query()
|
||||
.where({ webhookId })
|
||||
.orderBy('created_at', 'DESC');
|
||||
|
||||
return await paginate(query, pagination.page, pagination.limit);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get logs for webhook: ${webhookId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
generateSignature(payload: object, secret: string): string {
|
||||
const message = JSON.stringify(payload);
|
||||
return crypto.createHmac('sha256', secret).update(message).digest('hex');
|
||||
}
|
||||
|
||||
verifySignature(payload: object, signature: string, secret: string): boolean {
|
||||
const expectedSignature = this.generateSignature(payload, secret);
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expectedSignature),
|
||||
);
|
||||
}
|
||||
|
||||
async logWebhookAttempt(
|
||||
webhookId: string,
|
||||
eventType: string,
|
||||
payload: object,
|
||||
responseStatus: number | null,
|
||||
responseBody: string | null,
|
||||
responseTime: number | null,
|
||||
retryCount: number,
|
||||
): Promise<WebhookLog> {
|
||||
try {
|
||||
const status =
|
||||
responseStatus && responseStatus >= 200 && responseStatus < 300
|
||||
? WebhookLogStatus.SUCCESS
|
||||
: WebhookLogStatus.FAILED;
|
||||
|
||||
return await this.webhookLogRepository.query().insert({
|
||||
webhookId,
|
||||
eventType,
|
||||
payload: payload as any,
|
||||
responseStatus,
|
||||
responseBody,
|
||||
responseTime,
|
||||
retryCount,
|
||||
status: status as any,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to log webhook attempt: ${webhookId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user