Production hardening: CSP, rate limit, env validation, health gating, Render blueprint, DEPLOYMENT.md
This commit is contained in:
@@ -16,6 +16,8 @@ import { NextResponse } from "next/server";
|
||||
import { civi, verifyChecksum } from "@/lib/civicrm";
|
||||
import { allFields, ACTIVITY_TYPE_NAME, ACTIVITY_STAGE_FIELD, FORM_CONTACT_RELATIONSHIP } from "@/config/form";
|
||||
import type { SubmitPayload } from "@/types/form";
|
||||
import { appEnv, redact } from "@/lib/env";
|
||||
import { rateLimit, clientIp } from "@/lib/rate-limit";
|
||||
|
||||
function isStubMode(): boolean {
|
||||
return !(
|
||||
@@ -28,6 +30,21 @@ function isStubMode(): boolean {
|
||||
const FIELD_BY_NAME = new Map(allFields.map((f) => [f.name, f]));
|
||||
|
||||
export async function POST(req: Request) {
|
||||
// Rate limit per client IP. Generous default — 10 submissions per minute.
|
||||
const ip = clientIp(req);
|
||||
const rl = rateLimit(`submit:${ip}`, { capacity: 10, windowMs: 60_000 });
|
||||
if (!rl.allowed) {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many submissions. Please wait a moment and try again." },
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
"Retry-After": String(Math.ceil(rl.resetMs / 1000)),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let body: SubmitPayload;
|
||||
try {
|
||||
body = (await req.json()) as SubmitPayload;
|
||||
@@ -44,6 +61,23 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ ok: true, stub: true });
|
||||
}
|
||||
|
||||
try {
|
||||
return await runSubmit(cid, cs, values);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[submit] failure:", redact(msg));
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: appEnv().isProduction
|
||||
? "Could not save your check-in. Please try again, or contact your engagement coordinator."
|
||||
: `Save failed: ${redact(msg)}`,
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function runSubmit(cid: string, cs: string, values: Record<string, unknown>): Promise<NextResponse> {
|
||||
const ok = await verifyChecksum(cid, cs);
|
||||
if (!ok) {
|
||||
return NextResponse.json({ error: "Link is invalid or has expired." }, { status: 401 });
|
||||
|
||||
Reference in New Issue
Block a user