auth: forgot/reset password + magic link + Google/MS/LinkedIn buttons
Forgot-password flow at /forum/forgot-password (Supabase resetPassword) plus /forum/reset-password that catches the recovery session and lets the user set a new password. 'Forgot password?' link added to the login form. SocialAuthButtons component drops onto both login + register pages with three OAuth providers (Google, Microsoft / Azure, LinkedIn — Apple skipped per Ryan, can add later when iOS app ships) plus a no-password magic-link option that works zero-config via Supabase OTP. Register page now detects already-known subscribers via the new /api/auth/lookup-email endpoint and sends a magic-link login instead of forcing them to pick a password. They can set a password later from their account if they want. Provider-side setup required: register OAuth apps in Google Cloud Console / Microsoft Entra / LinkedIn Developers, paste client IDs + secrets into Supabase dashboard → Authentication → Providers, set redirect URL to https://broadcastbeat.com/auth/callback. Code is wired to receive them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
51
src/app/api/auth/lookup-email/route.ts
Normal file
51
src/app/api/auth/lookup-email/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user