Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/app/api/waitlist/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NextResponse } from "next/server";
import { getServiceSupabase } from "@/lib/supabase";

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

interface WaitlistPayload {
email?: string;
company_name?: string;
use_case?: string;
}

export async function POST(request: Request) {
try {
const body = (await request.json()) as WaitlistPayload;

const email = body.email?.trim().toLowerCase();
if (!email || !EMAIL_REGEX.test(email)) {
return NextResponse.json(
{ error: "A valid email address is required." },
{ status: 400 }
);
}
Comment on lines +14 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return 400 for malformed/non-object request bodies.

If JSON parsing fails or body is not an object, the handler falls into the catch block and returns 500. That should be a 400 client error.

🔧 Proposed fix
 export async function POST(request: Request) {
   try {
-    const body = (await request.json()) as WaitlistPayload;
+    let body: WaitlistPayload;
+    try {
+      const parsed = await request.json();
+      if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+        return NextResponse.json(
+          { error: "Invalid request body." },
+          { status: 400 }
+        );
+      }
+      body = parsed as WaitlistPayload;
+    } catch {
+      return NextResponse.json(
+        { error: "Invalid JSON body." },
+        { status: 400 }
+      );
+    }

Also applies to: 51-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/waitlist/route.ts` around lines 14 - 22, The handler currently
assumes request.json() returns a valid object and treats parse failures as
server errors; update the logic around the body parsing (the code that sets
const body = (await request.json()) as WaitlistPayload and the similar block
around lines referenced 51-56) to explicitly catch JSON parse errors and
validate that body is a non-null object before using body.email; if JSON.parse
fails or body is not an object return NextResponse.json({ error: "Malformed or
invalid request body." }, { status: 400 }) instead of letting it fall through to
the 500 catch, and keep the existing EMAIL_REGEX/email presence checks
afterward.


const company_name = body.company_name?.trim() || null;
const use_case = body.use_case?.trim() || null;

const supabase = getServiceSupabase();

const { error } = await supabase
.from("waitlist")
.insert({ email, company_name, use_case });

if (error) {
if (error.code === "23505") {
return NextResponse.json(
{ error: "This email is already on the waitlist." },
{ status: 409 }
);
}

console.error("[api/waitlist] Supabase error:", error.message);
return NextResponse.json(
Comment on lines +41 to +42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging raw error messages that may include submitted email.

Line 41 and Line 52 log raw error text/object. Database errors can contain duplicate key details with user email.

🔧 Proposed fix
-      console.error("[api/waitlist] Supabase error:", error.message);
+      console.error("[api/waitlist] Supabase error", {
+        code: error.code,
+        status: error.status,
+      });
@@
-    console.error("[api/waitlist] Unexpected error:", err);
+    console.error("[api/waitlist] Unexpected error", {
+      name: err instanceof Error ? err.name : "UnknownError",
+    });

Also applies to: 52-52

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/waitlist/route.ts` around lines 41 - 42, The console.error calls
in src/app/api/waitlist/route.ts are logging raw error objects/messages (seen
around the Supabase call and the second error path) which can contain user
emails; change those logs to avoid printing error.message/raw error objects.
Instead, log a sanitized, minimal message and non-sensitive fields (e.g.,
error.code or status) from the Supabase response and optionally a redacted/error
id; do not include the submitted email or full error text. Update the error
handling in the POST handler (the function handling the Supabase insert and the
subsequent NextResponse.json error path) to redact sensitive values (use a regex
to mask emails if you must include them) or omit them entirely and return a
generic NextResponse.json error to the client.

{
error: "Waitlist is temporarily unavailable. Please try again later.",
},
{ status: 503 }
);
}

return NextResponse.json({ ok: true }, { status: 201 });
} catch (err) {
console.error("[api/waitlist] Unexpected error:", err);
return NextResponse.json(
{ error: "Something went wrong. Please try again later." },
{ status: 500 }
);
}
}
7 changes: 2 additions & 5 deletions src/features/how-it-works/HowItWorks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Globe, Link2, ShieldCheck, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";

const ease = [0.4, 0, 0.2, 1] as const; // smooth ease-out
const OPEN_DELAY = 0.10; // mini delay before opening (seconds)
const OPEN_DELAY = 0.1; // mini delay before opening (seconds)
const OPEN_DURATION = 0.4; // slightly longer for smoother feel

type Step = {
Expand Down Expand Up @@ -211,10 +211,7 @@ export default function HowItWorks() {
const baseId = useId();

return (
<section
aria-labelledby="how-it-works-heading"
className="relative w-full"
>
<section aria-labelledby="how-it-works-heading" className="relative w-full">
{/* Section label */}
<p
id="how-it-works-label"
Expand Down
48 changes: 31 additions & 17 deletions src/features/waitlist/WaitlistForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { ShineBorder } from "@/components/ui/shine-border";

const FORMSPREE_ENDPOINT = "https://formspree.io/f/xyzndpdo";

type Status = "idle" | "ok" | "error";
type Status = "idle" | "ok" | "duplicate" | "error";

export default function WaitlistForm() {
const [email, setEmail] = useState("");
const [company, setCompany] = useState("");
const [message, setMessage] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [status, setStatus] = useState<Status>("idle");
const [errorMessage, setErrorMessage] = useState("");

const [botField, setBotField] = useState("");

Expand All @@ -32,41 +31,53 @@ export default function WaitlistForm() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("idle");
setErrorMessage("");

if (botField) return;

if (!validateEmail(email)) {
setStatus("error");
setErrorMessage("Please check your email and try again.");
return;
}

setIsSubmitting(true);

try {
const res = await fetch(FORMSPREE_ENDPOINT, {
const res = await fetch("/api/waitlist", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
company,
message,
_gotcha: botField,
_subject: "New waitlist signup · Acta",
page: typeof window !== "undefined" ? window.location.href : "",
company_name: company,
use_case: message,
}),
});

if (!res.ok) throw new Error("Submission failed");
if (res.status === 409) {
setStatus("duplicate");
return;
}

if (res.status === 400) {
setStatus("error");
setErrorMessage("Please check your email and try again.");
return;
}

if (!res.ok) {
setStatus("error");
setErrorMessage("Something went wrong. Please try again later.");
return;
}

setEmail("");
setCompany("");
setMessage("");
setStatus("ok");
} catch {
setStatus("error");
setErrorMessage("Something went wrong. Please try again later.");
} finally {
setIsSubmitting(false);
}
Expand Down Expand Up @@ -141,11 +152,14 @@ export default function WaitlistForm() {
Thank you! We will contact you soon.
</p>
)}
{status === "error" && (
<p className="text-sm text-red-500 text-center">
Something went wrong. Please check your email and try again.
{status === "duplicate" && (
<p className="text-sm text-yellow-400 text-center">
This email is already on the waitlist.
</p>
)}
{status === "error" && (
<p className="text-sm text-red-500 text-center">{errorMessage}</p>
)}
</form>
</CardContent>
<div className="pointer-events-none absolute inset-0 rounded-3xl shadow-[inset_0_0_60px_rgba(255,255,255,0.03)]" />
Expand Down
15 changes: 11 additions & 4 deletions src/lib/supabase.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { createClient, type SupabaseClient } from "@supabase/supabase-js";

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
// Placeholders for build/CI when env vars are unset; API will return 503 on insert.
// Not real credentials – Supabase calls will fail; avoids build errors when env is empty.
const PLACEHOLDER_URL = "https://placeholder.supabase.co";
const PLACEHOLDER_KEY = "placeholder-anon-key-not-a-secret";

const supabaseUrl =
process.env.NEXT_PUBLIC_SUPABASE_URL?.trim() || PLACEHOLDER_URL;
const supabaseAnonKey =
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY?.trim() || PLACEHOLDER_KEY;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim();

/**
* Public (anon) Supabase client – safe to use in both client and server code.
* Requires NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in env.
* Uses placeholders when env vars are unset (build/CI); inserts will fail with 503.
*/
export const supabase: SupabaseClient = createClient(
supabaseUrl,
Expand Down