GET /api/public/donor-eligibility?key=&email= returns only {eligible, tier}
(member/donor) — no names or dollar amounts — gated by PUBLIC_LOOKUP_SECRET,
rate-limited (30/min), and CORS-restricted to PUBLIC_LOOKUP_ORIGIN. Lets the
FluentForms checkout unlock a donor discount by email. Docs + ready-to-paste
form snippet in docs/fluentforms-donor-discount.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
import { timingSafeEqual } from "node:crypto";
|
|
import type { FastifyInstance } from "fastify";
|
|
|
|
function safeEqual(a: string, b: string): boolean {
|
|
const ba = Buffer.from(a || "");
|
|
const bb = Buffer.from(b || "");
|
|
if (ba.length !== bb.length) return false;
|
|
return timingSafeEqual(ba, bb);
|
|
}
|
|
|
|
/**
|
|
* Public, secret-gated donor-eligibility lookup for the FluentForms checkout.
|
|
* The form's JS calls this on email blur to decide whether to unlock a donor
|
|
* discount. Deliberately minimal: returns only { eligible, tier } — never
|
|
* names or dollar amounts — so even with the (page-source-visible) secret it
|
|
* can't leak donor financials. Rate-limited and CORS-restricted.
|
|
*/
|
|
export async function publicLookupRoutes(app: FastifyInstance): Promise<void> {
|
|
const cfg = app.ctx.config;
|
|
const origin = cfg.PUBLIC_LOOKUP_ORIGIN;
|
|
|
|
const cors = (reply: any) => {
|
|
reply.header("Access-Control-Allow-Origin", origin);
|
|
reply.header("Vary", "Origin");
|
|
reply.header("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
};
|
|
|
|
// Preflight (in case the form sends one).
|
|
app.options("/api/public/donor-eligibility", async (_req, reply) => {
|
|
cors(reply);
|
|
return reply.code(204).send();
|
|
});
|
|
|
|
app.get(
|
|
"/api/public/donor-eligibility",
|
|
{ config: { rateLimit: { max: 30, timeWindow: "1 minute" } } },
|
|
async (req, reply) => {
|
|
cors(reply);
|
|
// Disabled unless configured.
|
|
if (!cfg.PUBLIC_LOOKUP_SECRET || !app.ctx.donors.enabled) {
|
|
return reply.code(404).send({ error: "not_available" });
|
|
}
|
|
const { key, email } = (req.query ?? {}) as { key?: string; email?: string };
|
|
if (!key || !safeEqual(key, cfg.PUBLIC_LOOKUP_SECRET)) {
|
|
return reply.code(401).send({ error: "unauthorized" });
|
|
}
|
|
const addr = String(email ?? "").trim();
|
|
if (!addr) return { eligible: false, tier: null };
|
|
|
|
try {
|
|
const d = await app.ctx.donors.lookup(addr);
|
|
const tier = d.found ? (d.isMember ? "member" : "donor") : null;
|
|
return { eligible: d.found, tier };
|
|
} catch {
|
|
// Fail closed — no discount rather than an error the form can't handle.
|
|
return { eligible: false, tier: null };
|
|
}
|
|
},
|
|
);
|
|
}
|