import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { enforceRateLimit, limitWrite, requestIp } from "~/server/limits"; import { actOnAbuseReport, listAbuseReports, recordAbuseReport } from "~/server/abuse"; import { requireSiteAdmin } from "~/server/metering/access"; import { ABUSE_ACTIONS, ABUSE_CATEGORIES } from "~/lib/abuse"; const status = z.enum(["open", "actioned", "dismissed"]); export const submitAbuseReport = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( z.object({ url: z.url({ protocol: /^https?$/ }).max(2000), category: z.enum(ABUSE_CATEGORIES), details: z.string().max(4000), email: z.union([z.literal(""), z.email().max(320)]), }), ) .handler(async ({ data, context }) => { const ip = requestIp(); await enforceRateLimit("REPORT_LIMITER", `abuse-report:${ip}`); await recordAbuseReport({ source: "web", targetUrl: data.url, category: data.category, details: data.details, reporterEmail: data.email || null, reporterKey: context.viewer ? `user:${context.viewer.id}` : `ip:${ip}`, reporterKind: context.viewer ? "user" : "ip", }); return { ok: true }; }); export const getAbuseQueue = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(z.object({ status })) .handler(async ({ data, context }) => { requireSiteAdmin(requireViewer(context.viewer)); return { reports: await listAbuseReports(data.status) }; }); export const actOnAbuse = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( z.object({ id: z.string().min(1).max(64), action: z.enum(ABUSE_ACTIONS), reason: z.string().max(1000).nullable().default(null), }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); requireSiteAdmin(viewer); await limitWrite(viewer); await actOnAbuseReport(data.id, data.action, viewer.id, data.reason); return { ok: true }; });