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
223 lines
6.5 KiB
TypeScript
223 lines
6.5 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|