src/app/api/cf-stream/webhook/route.ts
Annotation non disponible
Lance npm run annotate (nécessite ANTHROPIC_API_KEY dans .env.local) pour générer une annotation française par Claude Haiku 4.5.
Concepts détectés — comprends la théorie
Route API Next.js
3 occurrencesCe fichier est une route API Next.js (App Router). Voir le contrat API complet pour les conventions de réponse et d'auth.
Voir l'article général
ORM Prisma
1 occurrenceCe fichier accède à la base de données via Prisma. Prisma est l'ORM utilisé côté backend pour les requêtes typées sur PostgreSQL.
Voir l'article général
2 exports
POSTmaxDuration
Code source· typescript
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { verifyWebhookSignature } from "@/lib/cloudflare-stream";
import { captureError } from "@/lib/sentry";
export const maxDuration = 30;
type CFStreamWebhookEvent = {
uid?: string;
status?: { state?: "queued" | "inprogress" | "ready" | "error" };
thumbnail?: string;
duration?: number;
playback?: { hls?: string; dash?: string };
input?: { width?: number; height?: number };
};
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get("webhook-signature");
const valid = await verifyWebhookSignature(rawBody, signature);
if (!valid) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
let event: CFStreamWebhookEvent;
try {
event = JSON.parse(rawBody) as CFStreamWebhookEvent;
} catch (e) {
captureError(e, {
route: "/api/cf-stream/webhook",
extra: { phase: "parse_webhook_json" },
});
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
if (!event.uid) {
return NextResponse.json({ ok: true, ignored: true });
}
if (event.status?.state === "ready") {
await prisma.media.updateMany({
where: { cloudflareStreamId: event.uid },
data: {
url: event.playback?.hls ?? "",
cloudflareReady: true,
thumbnailUrl: event.thumbnail ?? null,
duration: event.duration ?? null,
videoWidth: event.input?.width ?? null,
videoHeight: event.input?.height ?? null,
},
});
} else if (event.status?.state === "error") {
// Cleanup : supprimer la pré-création si jamais arrivée à READY
await prisma.media.deleteMany({
where: { cloudflareStreamId: event.uid, cloudflareReady: false },
});
}
return NextResponse.json({ ok: true });
}
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { verifyWebhookSignature } from "@/lib/cloudflare-stream";
import { captureError } from "@/lib/sentry";
export const maxDuration = 30;
type CFStreamWebhookEvent = {
uid?: string;
status?: { state?: "queued" | "inprogress" | "ready" | "error" };
thumbnail?: string;
duration?: number;
playback?: { hls?: string; dash?: string };
input?: { width?: number; height?: number };
};
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get("webhook-signature");
const valid = await verifyWebhookSignature(rawBody, signature);
if (!valid) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
let event: CFStreamWebhookEvent;
try {
event = JSON.parse(rawBody) as CFStreamWebhookEvent;
} catch (e) {
captureError(e, {
route: "/api/cf-stream/webhook",
extra: { phase: "parse_webhook_json" },
});
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
if (!event.uid) {
return NextResponse.json({ ok: true, ignored: true });
}
if (event.status?.state === "ready") {
await prisma.media.updateMany({
where: { cloudflareStreamId: event.uid },
data: {
url: event.playback?.hls ?? "",
cloudflareReady: true,
thumbnailUrl: event.thumbnail ?? null,
duration: event.duration ?? null,
videoWidth: event.input?.width ?? null,
videoHeight: event.input?.height ?? null,
},
});
} else if (event.status?.state === "error") {
// Cleanup : supprimer la pré-création si jamais arrivée à READY
await prisma.media.deleteMany({
where: { cloudflareStreamId: event.uid, cloudflareReady: false },
});
}
return NextResponse.json({ ok: true });
}