update
This commit is contained in:
309
ts/bunq.classes.webhook.ts
Normal file
309
ts/bunq.classes.webhook.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import * as plugins from './bunq.plugins';
|
||||
import { BunqAccount } from './bunq.classes.account';
|
||||
import { BunqNotification, BunqWebhookHandler } from './bunq.classes.notification';
|
||||
import { BunqCrypto } from './bunq.classes.crypto';
|
||||
|
||||
/**
|
||||
* Webhook server for receiving bunq notifications
|
||||
*/
|
||||
export class BunqWebhookServer {
|
||||
private bunqAccount: BunqAccount;
|
||||
private notification: BunqNotification;
|
||||
private handler: BunqWebhookHandler;
|
||||
private server?: any; // HTTP server instance
|
||||
private port: number;
|
||||
private path: string;
|
||||
private publicUrl: string;
|
||||
|
||||
constructor(
|
||||
bunqAccount: BunqAccount,
|
||||
options: {
|
||||
port?: number;
|
||||
path?: string;
|
||||
publicUrl: string;
|
||||
}
|
||||
) {
|
||||
this.bunqAccount = bunqAccount;
|
||||
this.notification = new BunqNotification(bunqAccount);
|
||||
this.handler = new BunqWebhookHandler();
|
||||
this.port = options.port || 3000;
|
||||
this.path = options.path || '/webhook';
|
||||
this.publicUrl = options.publicUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the webhook server
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// Create HTTP server
|
||||
const http = await import('http');
|
||||
|
||||
this.server = http.createServer(async (req, res) => {
|
||||
if (req.method === 'POST' && req.url === this.path) {
|
||||
let body = '';
|
||||
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
// Get signature from headers
|
||||
const signature = req.headers['x-bunq-server-signature'] as string;
|
||||
|
||||
if (!signature) {
|
||||
res.statusCode = 401;
|
||||
res.end('Missing signature');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
const isValid = this.notification.verifyWebhookSignature(body, signature);
|
||||
|
||||
if (!isValid) {
|
||||
res.statusCode = 401;
|
||||
res.end('Invalid signature');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and process notification
|
||||
const notification = JSON.parse(body);
|
||||
await this.handler.process(notification);
|
||||
|
||||
res.statusCode = 200;
|
||||
res.end('OK');
|
||||
} catch (error) {
|
||||
console.error('Webhook processing error:', error);
|
||||
res.statusCode = 500;
|
||||
res.end('Internal server error');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end('Not found');
|
||||
}
|
||||
});
|
||||
|
||||
this.server.listen(this.port, () => {
|
||||
console.log(`Webhook server listening on port ${this.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the webhook server
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
if (this.server) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.server.close(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.server = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the webhook handler
|
||||
*/
|
||||
public getHandler(): BunqWebhookHandler {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register webhook with bunq
|
||||
*/
|
||||
public async register(categories?: string[]): Promise<void> {
|
||||
const webhookUrl = `${this.publicUrl}${this.path}`;
|
||||
|
||||
if (categories && categories.length > 0) {
|
||||
// Register specific categories
|
||||
const filters = categories.map(category => ({
|
||||
category,
|
||||
notificationTarget: webhookUrl
|
||||
}));
|
||||
|
||||
await this.notification.createMultipleUrlFilters(filters);
|
||||
} else {
|
||||
// Register all payment and account events
|
||||
await this.notification.setupPaymentWebhook(webhookUrl);
|
||||
await this.notification.setupAccountWebhook(webhookUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all webhooks
|
||||
*/
|
||||
public async unregister(): Promise<void> {
|
||||
await this.notification.clearAllUrlFilters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook client for sending test notifications
|
||||
*/
|
||||
export class BunqWebhookClient {
|
||||
private crypto: BunqCrypto;
|
||||
private privateKey: string;
|
||||
|
||||
constructor(privateKey: string) {
|
||||
this.crypto = new BunqCrypto();
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a test notification to a webhook endpoint
|
||||
*/
|
||||
public async sendTestNotification(
|
||||
webhookUrl: string,
|
||||
notification: any
|
||||
): Promise<void> {
|
||||
const body = JSON.stringify(notification);
|
||||
|
||||
// Create signature
|
||||
const sign = plugins.crypto.createSign('SHA256');
|
||||
sign.update(body);
|
||||
sign.end();
|
||||
const signature = sign.sign(this.privateKey, 'base64');
|
||||
|
||||
// Send request
|
||||
const response = await plugins.smartrequest.request(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Bunq-Server-Signature': signature
|
||||
},
|
||||
requestBody: body
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`Webhook request failed with status ${response.statusCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test payment notification
|
||||
*/
|
||||
public createTestPaymentNotification(paymentData: any): any {
|
||||
return {
|
||||
NotificationUrl: {
|
||||
target_url: 'https://example.com/webhook',
|
||||
category: 'PAYMENT',
|
||||
event_type: 'PAYMENT_CREATED',
|
||||
object: {
|
||||
Payment: {
|
||||
id: 1234,
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
monetary_account_id: 1,
|
||||
amount: {
|
||||
currency: 'EUR',
|
||||
value: '10.00'
|
||||
},
|
||||
description: 'Test payment',
|
||||
type: 'IDEAL',
|
||||
...paymentData
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test account notification
|
||||
*/
|
||||
public createTestAccountNotification(accountData: any): any {
|
||||
return {
|
||||
NotificationUrl: {
|
||||
target_url: 'https://example.com/webhook',
|
||||
category: 'MONETARY_ACCOUNT',
|
||||
event_type: 'MONETARY_ACCOUNT_UPDATED',
|
||||
object: {
|
||||
MonetaryAccountBank: {
|
||||
id: 1234,
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
balance: {
|
||||
currency: 'EUR',
|
||||
value: '100.00'
|
||||
},
|
||||
...accountData
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook event types
|
||||
*/
|
||||
export enum BunqWebhookEventType {
|
||||
// Payment events
|
||||
PAYMENT_CREATED = 'PAYMENT_CREATED',
|
||||
PAYMENT_UPDATED = 'PAYMENT_UPDATED',
|
||||
PAYMENT_CANCELLED = 'PAYMENT_CANCELLED',
|
||||
|
||||
// Account events
|
||||
MONETARY_ACCOUNT_CREATED = 'MONETARY_ACCOUNT_CREATED',
|
||||
MONETARY_ACCOUNT_UPDATED = 'MONETARY_ACCOUNT_UPDATED',
|
||||
MONETARY_ACCOUNT_CLOSED = 'MONETARY_ACCOUNT_CLOSED',
|
||||
|
||||
// Card events
|
||||
CARD_CREATED = 'CARD_CREATED',
|
||||
CARD_UPDATED = 'CARD_UPDATED',
|
||||
CARD_CANCELLED = 'CARD_CANCELLED',
|
||||
CARD_TRANSACTION = 'CARD_TRANSACTION',
|
||||
|
||||
// Request events
|
||||
REQUEST_INQUIRY_CREATED = 'REQUEST_INQUIRY_CREATED',
|
||||
REQUEST_INQUIRY_UPDATED = 'REQUEST_INQUIRY_UPDATED',
|
||||
REQUEST_INQUIRY_ACCEPTED = 'REQUEST_INQUIRY_ACCEPTED',
|
||||
REQUEST_INQUIRY_REJECTED = 'REQUEST_INQUIRY_REJECTED',
|
||||
|
||||
// Other events
|
||||
SCHEDULE_RESULT = 'SCHEDULE_RESULT',
|
||||
TAB_RESULT = 'TAB_RESULT',
|
||||
DRAFT_PAYMENT_CREATED = 'DRAFT_PAYMENT_CREATED',
|
||||
DRAFT_PAYMENT_UPDATED = 'DRAFT_PAYMENT_UPDATED'
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook middleware for Express.js
|
||||
*/
|
||||
export function bunqWebhookMiddleware(
|
||||
bunqAccount: BunqAccount,
|
||||
handler: BunqWebhookHandler
|
||||
) {
|
||||
const notification = new BunqNotification(bunqAccount);
|
||||
|
||||
return async (req: any, res: any, next: any) => {
|
||||
try {
|
||||
// Get signature from headers
|
||||
const signature = req.headers['x-bunq-server-signature'];
|
||||
|
||||
if (!signature) {
|
||||
res.status(401).send('Missing signature');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get raw body
|
||||
const body = JSON.stringify(req.body);
|
||||
|
||||
// Verify signature
|
||||
const isValid = notification.verifyWebhookSignature(body, signature);
|
||||
|
||||
if (!isValid) {
|
||||
res.status(401).send('Invalid signature');
|
||||
return;
|
||||
}
|
||||
|
||||
// Process notification
|
||||
await handler.process(req.body);
|
||||
|
||||
res.status(200).send('OK');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user