diff --git a/src/app/api/auth/lookup-email/route.ts b/src/app/api/auth/lookup-email/route.ts new file mode 100644 index 0000000..b1aac52 --- /dev/null +++ b/src/app/api/auth/lookup-email/route.ts @@ -0,0 +1,51 @@ +/** + * GET /api/auth/lookup-email?email=foo@example.com + * + * Returns { known: true|false }. "Known" means the email is already on our + * subscriber list (bb.email_subscribers) OR has an auth.users entry. Used + * by the register page so existing newsletter readers get a magic-link + * login flow instead of being asked to create a new account. + * + * Privacy: returns only known/not-known, never any details — does not + * leak account existence beyond a binary signal (necessary for the UX). + * Rate-limited at the proxy layer. + */ +import { NextRequest, NextResponse } from "next/server"; +import { createClient as createService } from "@supabase/supabase-js"; + +export async function GET(req: NextRequest) { + const email = (new URL(req.url).searchParams.get("email") || "").trim().toLowerCase(); + if (!email || !email.includes("@")) { + return NextResponse.json({ known: false }, { status: 400 }); + } + + const url = process.env.NEXT_PUBLIC_SUPABASE_URL || ""; + const key = process.env.SUPABASE_SERVICE_ROLE_KEY || ""; + if (!url || !key) { + return NextResponse.json({ known: false }); + } + const svc = createService(url, key, { auth: { persistSession: false } }); + + // Layer 1 — the rich subscriber registry (95k+ rows) + const { data: sub } = await svc + .schema("bb") + .from("email_subscribers") + .select("id, status") + .eq("email_domain", email.split("@")[1]) + .ilike("email", email) + .limit(1) + .maybeSingle(); + if (sub) return NextResponse.json({ known: true, source: "subscriber" }); + + // Layer 2 — the lean property-specific list + const { data: nl } = await svc + .schema("bb") + .from("newsletter_subscribers") + .select("id") + .ilike("email", email) + .limit(1) + .maybeSingle(); + if (nl) return NextResponse.json({ known: true, source: "newsletter" }); + + return NextResponse.json({ known: false }); +} diff --git a/src/app/forum/forgot-password/page.tsx b/src/app/forum/forgot-password/page.tsx new file mode 100644 index 0000000..9301c73 --- /dev/null +++ b/src/app/forum/forgot-password/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { Suspense, useState } from "react"; +import Link from "next/link"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import { createClient } from "@/lib/supabase/client"; + +function ForgotInner() { + const supabase = createClient(); + const [email, setEmail] = useState(""); + const [busy, setBusy] = useState(false); + const [msg, setMsg] = useState(null); + const [err, setErr] = useState(null); + + async function submit(e: React.FormEvent) { + e.preventDefault(); + setBusy(true); setMsg(null); setErr(null); + const { error } = await supabase.auth.resetPasswordForEmail(email.trim(), { + redirectTo: `${window.location.origin}/forum/reset-password`, + }); + setBusy(false); + if (error) { + setErr(error.message); + } else { + setMsg("Check your inbox — we sent a password reset link. If you didn't have an account, no email is sent."); + } + } + + return ( + <> +
+
+
+
+
Account recovery
+

Reset your password

+

+ Enter your email — we’ll send a link to set a new password. +

+
+
+ {err && ( +
+ {err} +
+ )} + {msg && ( +
+ {msg} +
+ )} +
+ + setEmail(e.target.value)} + autoComplete="email" + required + className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" + /> +
+ +
+ Remembered it?{" "} + + Back to sign in + +
+
+
+
+