SPB Git
2.7 KB · 48 lines typescript
1import "server-only";2import { NextResponse, type NextRequest } from "next/server";3import { z } from "zod";4import { getSession, assertSameOrigin, AuthError } from "./auth";56type Handler<P> = (req: NextRequest, ctx: { params: P; session: { id: string } }) => Promise<Response>;78/** Wraps a route handler: requires a valid session, checks Origin on mutations, maps errors to JSON. */9export function guarded<P = Record<string, string>>(fn: Handler<P>) {10  return async (req: NextRequest, ctx: { params: Promise<P> }): Promise<Response> => {11    try {12      const session = await getSession();13      if (!session) throw new AuthError();14      if (req.method !== "GET" && req.method !== "HEAD") await assertSameOrigin();15      return await fn(req, { params: await ctx.params, session });16    } catch (e) { return errorResponse(e); }17  };18}1920export function errorResponse(e: unknown): Response {21  const err = e as { status?: number; message?: string; issues?: unknown };22  if (e instanceof z.ZodError) return NextResponse.json({ error: "Requête invalide", issues: e.issues }, { status: 400 });23  const status = typeof err.status === "number" && err.status >= 400 && err.status < 600 ? err.status : 500;24  if (status >= 500) console.error(e);25  return NextResponse.json({ error: err.message ?? "Erreur interne" }, { status });26}2728export const json = (data: unknown, init?: ResponseInit) => NextResponse.json(data, init);2930export async function body<T extends z.ZodTypeAny>(req: NextRequest, schema: T): Promise<z.infer<T>> {31  return schema.parse(await req.json());32}3334/** Server-Sent Events response. `run` receives an emit function and must resolve when finished. */35export function sse(run: (emit: (event: string, data: unknown) => void, signal: AbortSignal) => Promise<void>, signal: AbortSignal): Response {36  const enc = new TextEncoder();37  let closed = false;38  const stream = new ReadableStream({39    start(controller) {40      const emit = (event: string, data: unknown) => { if (closed) return; try { controller.enqueue(enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)); } catch { closed = true; } };41      const ping = setInterval(() => { if (!closed) try { controller.enqueue(enc.encode(`: ping\n\n`)); } catch { closed = true; } }, 15_000);42      run(emit, signal).catch(e => emit("error", { message: (e as Error).message ?? String(e) })).finally(() => { clearInterval(ping); closed = true; try { controller.close(); } catch { /* already closed */ } });43    },44    cancel() { closed = true; },45  });46  return new Response(stream, { headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no" } });47}48
Files · e271e75196f58c092c7f0be7ddb52ff27d11b54f
Menu

Theme