Files
gridpilot.gg/apps/website/app/api/signup/route.ts
2025-12-11 21:06:25 +01:00

114 lines
2.8 KiB
TypeScript

import { NextResponse } from 'next/server';
import { validateEmail, isDisposableEmail } from '@gridpilot/identity/domain/value-objects/EmailAddress';
import { checkRateLimit, getClientIp } from '@/lib/rate-limit';
const SIGNUP_DEV_STORE = new Map<string, { email: string; createdAt: number; ip: string }>();
const SIGNUP_KV_HASH_KEY = 'signups:emails';
const isDev = !process.env.KV_REST_API_URL;
function jsonError(status: number, message: string, extra: Record<string, unknown> = {}) {
return NextResponse.json(
{
error: message,
...extra,
},
{ status },
);
}
export async function POST(request: Request) {
let body: unknown;
try {
body = await request.json();
} catch {
return jsonError(400, 'Invalid request body');
}
const email =
typeof body === 'object' && body !== null && 'email' in body
? (body as { email: unknown }).email
: undefined;
if (typeof email !== 'string' || !email.trim()) {
return jsonError(400, 'Invalid email address');
}
const validation = validateEmail(email);
if (!validation.success) {
return jsonError(400, validation.error || 'Invalid email address');
}
const normalizedEmail = validation.email;
if (isDisposableEmail(normalizedEmail)) {
return jsonError(400, 'Disposable email addresses are not allowed');
}
const ip = getClientIp(request);
try {
const rateResult = await checkRateLimit(ip);
if (!rateResult.allowed) {
const retryAfterSeconds = Math.max(0, Math.round((rateResult.resetAt - Date.now()) / 1000));
return jsonError(429, 'Too many signups, please try again later.', {
retryAfter: retryAfterSeconds,
});
}
} catch {
return jsonError(503, 'Temporarily unable to accept signups.');
}
try {
if (isDev) {
const existing = SIGNUP_DEV_STORE.get(normalizedEmail);
if (existing) {
return jsonError(409, 'You are already on the list.');
}
SIGNUP_DEV_STORE.set(normalizedEmail, {
email: normalizedEmail,
createdAt: Date.now(),
ip,
});
} else {
const { kv } = await import('@vercel/kv');
const existing = await kv.hget<{ email: string; createdAt: number; ip: string }>(
SIGNUP_KV_HASH_KEY,
normalizedEmail,
);
if (existing) {
return jsonError(409, 'You are already on the list.');
}
await kv.hset(SIGNUP_KV_HASH_KEY, {
[normalizedEmail]: {
email: normalizedEmail,
createdAt: Date.now(),
ip,
},
});
}
} catch (error) {
console.error('Signup storage error:', error);
return jsonError(503, 'Temporarily unable to accept signups.');
}
return NextResponse.json(
{
ok: true,
message: 'You are on the grid! We will be in touch soon.',
},
{
status: 201,
},
);
}