wip
This commit is contained in:
111
apps/website/app/api/signup/route.ts
Normal file
111
apps/website/app/api/signup/route.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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 = (body as any)?.email;
|
||||
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user