feat(gatekeeper): add PR merge automation service
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
This commit is contained in:
67
apps/api/src/gatekeeper/gatekeeper.controller.ts
Normal file
67
apps/api/src/gatekeeper/gatekeeper.controller.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Body, Controller, Headers, Logger, Post, Req, type RawBodyRequest } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
import type { Request } from "express";
|
||||
import { SkipCsrf } from "../common/decorators/skip-csrf.decorator";
|
||||
import { GiteaPrWebhookDto } from "./dto/gitea-pr-webhook.dto";
|
||||
import { GatekeeperService } from "./gatekeeper.service";
|
||||
|
||||
@Controller("gatekeeper/webhook")
|
||||
export class GatekeeperController {
|
||||
private readonly logger = new Logger(GatekeeperController.name);
|
||||
|
||||
constructor(
|
||||
private readonly gatekeeperService: GatekeeperService,
|
||||
private readonly configService: ConfigService
|
||||
) {}
|
||||
|
||||
@SkipCsrf()
|
||||
@Post("gitea")
|
||||
handleWebhook(
|
||||
@Req() req: RawBodyRequest<Request>,
|
||||
@Body() body: GiteaPrWebhookDto,
|
||||
@Headers("x-gitea-signature") signature: string | undefined
|
||||
): Promise<{ ok: boolean }> {
|
||||
const secret = this.configService.get<string>("GITEA_WEBHOOK_SECRET");
|
||||
|
||||
if (secret && !this.isValidSignature(this.getRequestBody(req, body), signature, secret)) {
|
||||
this.logger.warn("Received invalid Gitea webhook signature");
|
||||
return Promise.resolve({ ok: true });
|
||||
}
|
||||
|
||||
if (!secret) {
|
||||
this.logger.warn("GITEA_WEBHOOK_SECRET is not configured; accepting Gitea webhook");
|
||||
}
|
||||
|
||||
void this.gatekeeperService.handlePrEvent(body).catch((error: unknown) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Failed to process Gitea PR webhook: ${message}`);
|
||||
});
|
||||
|
||||
return Promise.resolve({ ok: true });
|
||||
}
|
||||
|
||||
private getRequestBody(req: RawBodyRequest<Request>, body: GiteaPrWebhookDto): Buffer {
|
||||
if (Buffer.isBuffer(req.rawBody)) {
|
||||
return req.rawBody;
|
||||
}
|
||||
|
||||
return Buffer.from(JSON.stringify(body));
|
||||
}
|
||||
|
||||
private isValidSignature(body: Buffer, signature: string | undefined, secret: string): boolean {
|
||||
if (!signature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expected = createHmac("sha256", secret).update(body).digest("hex");
|
||||
const actual = Buffer.from(signature);
|
||||
const expectedBuffer = Buffer.from(expected);
|
||||
|
||||
if (actual.length !== expectedBuffer.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return timingSafeEqual(actual, expectedBuffer);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user