diff options
Diffstat (limited to 'apps/web/src/app')
| -rw-r--r-- | apps/web/src/app/api/waitlist/export/route.ts | 83 | ||||
| -rw-r--r-- | apps/web/src/app/layout.tsx | 4 |
2 files changed, 87 insertions, 0 deletions
diff --git a/apps/web/src/app/api/waitlist/export/route.ts b/apps/web/src/app/api/waitlist/export/route.ts new file mode 100644 index 00000000..0200e255 --- /dev/null +++ b/apps/web/src/app/api/waitlist/export/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from "next/server";
+import { baseRateLimit } from "@/lib/rate-limit";
+import { db, exportWaitlist, eq } from "@opencut/db";
+import { randomUUID } from "crypto";
+import {
+ exportWaitlistSchema,
+ exportWaitlistResponseSchema,
+} from "@/lib/schemas/waitlist";
+
+const requestSchema = exportWaitlistSchema;
+const responseSchema = exportWaitlistResponseSchema;
+
+export async function POST(request: NextRequest) {
+ try {
+ const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
+ const { success } = await baseRateLimit.limit(ip);
+ if (!success) {
+ return NextResponse.json({ error: "Too many requests" }, { status: 429 });
+ }
+
+ const body = await request.json().catch(() => null);
+ if (!body) {
+ return NextResponse.json(
+ { error: "Invalid JSON in request body" },
+ { status: 400 }
+ );
+ }
+
+ const parsed = requestSchema.safeParse(body);
+ if (!parsed.success) {
+ return NextResponse.json(
+ {
+ error: "Invalid request parameters",
+ details: parsed.error.flatten().fieldErrors,
+ },
+ { status: 400 }
+ );
+ }
+
+ const { email } = parsed.data;
+
+ const existing = await db
+ .select({ id: exportWaitlist.id })
+ .from(exportWaitlist)
+ .where(eq(exportWaitlist.email, email))
+ .limit(1);
+
+ if (existing.length > 0) {
+ const responseData = { success: true, alreadySubscribed: true } as const;
+ const validated = responseSchema.safeParse(responseData);
+ if (!validated.success) {
+ return NextResponse.json(
+ { error: "Internal response formatting error" },
+ { status: 500 }
+ );
+ }
+ return NextResponse.json(validated.data);
+ }
+
+ await db.insert(exportWaitlist).values({
+ id: randomUUID(),
+ email,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ });
+
+ const responseData = { success: true } as const;
+ const validated = responseSchema.safeParse(responseData);
+ if (!validated.success) {
+ return NextResponse.json(
+ { error: "Internal response formatting error" },
+ { status: 500 }
+ );
+ }
+ return NextResponse.json(validated.data);
+ } catch (error) {
+ console.error("Waitlist API error:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index c5a9bf45..ee0bdbd2 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -17,6 +17,10 @@ const protectedRoutes = [ path: "/none", method: "GET", }, + { + path: "/api/waitlist/export", + method: "POST", + }, ]; export default function RootLayout({ |
