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>
122 lines
4.8 KiB
TypeScript
122 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import Header from "@/components/Header";
|
|
import Footer from "@/components/Footer";
|
|
import { createClient } from "@/lib/supabase/client";
|
|
|
|
function ResetInner() {
|
|
const router = useRouter();
|
|
const supabase = createClient();
|
|
const [hasSession, setHasSession] = useState<boolean | null>(null);
|
|
const [password, setPassword] = useState("");
|
|
const [confirm, setConfirm] = useState("");
|
|
const [busy, setBusy] = useState(false);
|
|
const [msg, setMsg] = useState<string | null>(null);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
// The recovery flow lands here with a fresh session (Supabase verifies
|
|
// the token in the URL hash and signs the user in). Confirm we got it.
|
|
useEffect(() => {
|
|
supabase.auth.getSession().then(({ data }) => {
|
|
setHasSession(!!data.session);
|
|
});
|
|
}, [supabase]);
|
|
|
|
async function submit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setBusy(true); setMsg(null); setErr(null);
|
|
if (password.length < 8) { setErr("Use at least 8 characters."); setBusy(false); return; }
|
|
if (password !== confirm) { setErr("Passwords don't match."); setBusy(false); return; }
|
|
const { error } = await supabase.auth.updateUser({ password });
|
|
setBusy(false);
|
|
if (error) { setErr(error.message); return; }
|
|
setMsg("Password updated. Redirecting…");
|
|
setTimeout(() => router.push("/forum"), 1200);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Header />
|
|
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]">
|
|
<div className="max-w-md mx-auto px-4 py-16">
|
|
<div className="text-center mb-6">
|
|
<div className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-mono mb-2">Reset password</div>
|
|
<h1 className="text-3xl font-heading font-bold text-white">Set a new password</h1>
|
|
</div>
|
|
{hasSession === false ? (
|
|
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 text-center">
|
|
<p className="text-[#cbd5e1] mb-4 text-sm">
|
|
This reset link is expired or invalid.
|
|
</p>
|
|
<Link
|
|
href="/forum/forgot-password"
|
|
className="inline-block bg-[#3b82f6] hover:bg-[#2563eb] text-white font-semibold text-sm px-5 py-2 rounded"
|
|
>
|
|
Request a new link
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<form
|
|
onSubmit={submit}
|
|
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl"
|
|
>
|
|
{err && (
|
|
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
|
|
{err}
|
|
</div>
|
|
)}
|
|
{msg && (
|
|
<div className="bg-[#0a1f2a] border border-[#1e6c8c] text-[#7adbff] font-body text-sm px-3 py-2 rounded">
|
|
{msg}
|
|
</div>
|
|
)}
|
|
<div>
|
|
<label className="block text-xs font-body uppercase tracking-wider text-[#888] mb-1.5">
|
|
New password
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
autoComplete="new-password"
|
|
minLength={8}
|
|
required
|
|
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#3b82f6]"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-body uppercase tracking-wider text-[#888] mb-1.5">
|
|
Confirm password
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={confirm}
|
|
onChange={(e) => setConfirm(e.target.value)}
|
|
autoComplete="new-password"
|
|
required
|
|
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#3b82f6]"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={busy}
|
|
className="w-full bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-semibold text-sm px-5 py-3 rounded-lg"
|
|
>
|
|
{busy ? "Saving…" : "Update password"}
|
|
</button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</main>
|
|
<Footer />
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function ResetPasswordPage() {
|
|
return <Suspense fallback={null}><ResetInner /></Suspense>;
|
|
}
|