first commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Controller,
|
||||
Headers,
|
||||
HttpCode,
|
||||
Post,
|
||||
RawBodyRequest,
|
||||
Req,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IdentityService } from '@task-stream/api/identity/application';
|
||||
import { Request } from 'express';
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||
import { Public } from '../auth/auth.decorators';
|
||||
|
||||
/** Receives `user.deactivated` / `token.revoked` from Pireon. */
|
||||
@Controller('webhooks/pireon')
|
||||
export class PireonWebhookController {
|
||||
constructor(
|
||||
private readonly identity: IdentityService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@Post()
|
||||
@HttpCode(204)
|
||||
async receive(
|
||||
@Req() req: RawBodyRequest<Request>,
|
||||
@Headers('x-webhook-signature') signature?: string,
|
||||
): Promise<void> {
|
||||
this.verifySignature(req.rawBody, signature);
|
||||
const body = req.body as {
|
||||
event?: string;
|
||||
type?: string;
|
||||
data?: unknown;
|
||||
payload?: unknown;
|
||||
};
|
||||
const event = body.event ?? body.type;
|
||||
if (!event) throw new BadRequestException('Missing event name');
|
||||
const data = (body.data ?? body.payload ?? body) as Record<string, unknown>;
|
||||
await this.identity.handleWebhook(event, data);
|
||||
}
|
||||
|
||||
/** X-Webhook-Signature = HMAC-SHA256(raw body, PIREON_WEBHOOK_SECRET). */
|
||||
private verifySignature(
|
||||
rawBody: Buffer | undefined,
|
||||
signature: string | undefined,
|
||||
): void {
|
||||
const secret = this.config.get<string>('PIREON_WEBHOOK_SECRET');
|
||||
if (!secret) {
|
||||
throw new UnauthorizedException(
|
||||
'Webhooks are disabled (PIREON_WEBHOOK_SECRET not set)',
|
||||
);
|
||||
}
|
||||
if (!rawBody || !signature)
|
||||
throw new UnauthorizedException('Missing signature');
|
||||
|
||||
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
|
||||
const received = signature.replace(/^sha256=/, '');
|
||||
const valid =
|
||||
received.length === expected.length &&
|
||||
timingSafeEqual(Buffer.from(received), Buffer.from(expected));
|
||||
if (!valid) throw new UnauthorizedException('Invalid signature');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user