41 lines
2.1 KiB
TypeScript
41 lines
2.1 KiB
TypeScript
import type { FastifyPluginAsync } from 'fastify';
|
|
import { z } from 'zod';
|
|
import { recordAudit } from '../audit.js';
|
|
import { config } from '../config.js';
|
|
import { pool } from '../db/pool.js';
|
|
import { bunnyMediaStatus, isBunnyWebhookConfigured, verifyBunnyWebhookSignature } from '../providers/bunny.js';
|
|
|
|
const webhookSchema = z.object({
|
|
VideoGuid: z.string().uuid(),
|
|
VideoLibraryId: z.coerce.number().int().positive(),
|
|
Status: z.coerce.number().int().nonnegative(),
|
|
Length: z.coerce.number().nonnegative().optional(),
|
|
});
|
|
|
|
export const bunnyWebhookRoutes: FastifyPluginAsync = async (app) => {
|
|
app.post('/bunny', async (request, reply) => {
|
|
if (!isBunnyWebhookConfigured()) return reply.code(503).send({ error: 'Os webhooks do Bunny não estão configurados.' });
|
|
const rawBody = (request as typeof request & { rawBody?: Buffer }).rawBody;
|
|
const signatureHeader = request.headers.signature || request.headers['x-bunny-signature'];
|
|
const signature = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
|
|
if (!rawBody || !verifyBunnyWebhookSignature(rawBody, signature)) {
|
|
return reply.code(401).send({ error: 'Assinatura do webhook do Bunny inválida.' });
|
|
}
|
|
const input = webhookSchema.parse(request.body);
|
|
if (input.VideoLibraryId !== config.BUNNY_STREAM_LIBRARY_ID) {
|
|
return reply.code(400).send({ error: 'Biblioteca de vídeos Bunny inesperada.' });
|
|
}
|
|
const status = bunnyMediaStatus(input.Status);
|
|
await pool.query(
|
|
`update lesson_media
|
|
set status = $2::media_status,
|
|
duration_seconds = coalesce($3, duration_seconds),
|
|
metadata = metadata || jsonb_build_object('bunnyStatus', $4, 'bunnyWebhookAt', now())
|
|
where provider = 'bunny' and external_id = $1`,
|
|
[input.VideoGuid, status, input.Length ? Math.round(input.Length) : null, input.Status],
|
|
);
|
|
await recordAudit({ action: 'media.bunny.webhook_received', subjectType: 'video', subjectId: input.VideoGuid, metadata: { status, providerStatus: input.Status }, ipAddress: request.ip });
|
|
return reply.code(204).send();
|
|
});
|
|
};
|