- API: require auth on thread+reply POST (401 if !user); lazy-create forum_user_profile via service-role; author_name = display_name - UI: surface API errors (kill silent catch); auto-redirect to login on 401; gate new-thread + reply forms with "Sign in to post" CTA - /forum/login + /forum/register: forum-themed Supabase Auth pages with ?next= redirect support - /forum/profile/edit + /api/forum/profile/me: edit own forum profile (username, display_name, bio, role, company, location, avatar) - Header: logout button + sign-in/join links when not authed - forum/user/[username]: show Edit Profile button when viewing own Fixes posting bug: RLS rejected anon inserts with cryptic message that the UI silently swallowed; now blocked with friendly 401 + redirect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
'use client';
|
|
|
|
import { Suspense, useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import Header from '@/components/Header';
|
|
import Footer from '@/components/Footer';
|
|
import { createClient } from '@/lib/supabase/client';
|
|
|
|
function safeNext(next: string | null): string {
|
|
if (!next) return '/forum';
|
|
if (!next.startsWith('/') || next.startsWith('//')) return '/forum';
|
|
return next;
|
|
}
|
|
|
|
function ForumLoginInner() {
|
|
const router = useRouter();
|
|
const params = useSearchParams();
|
|
const next = safeNext(params.get('next'));
|
|
const supabase = createClient();
|
|
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
supabase.auth.getUser().then(({ data: { user } }) => {
|
|
if (user) router.replace(next);
|
|
});
|
|
}, [next, router, supabase]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError(null);
|
|
if (!email || !password) {
|
|
setError('Email and password are required.');
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
const { error: signInError } = await supabase.auth.signInWithPassword({ email, password });
|
|
setLoading(false);
|
|
if (signInError) {
|
|
setError(signInError.message);
|
|
return;
|
|
}
|
|
window.location.href = next;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Header />
|
|
<main className="min-h-screen bg-[#111111]">
|
|
<div className="max-w-md mx-auto px-4 py-12">
|
|
<div className="text-center mb-6">
|
|
<h1 className="text-2xl font-heading font-bold text-white">Sign in to the Forum</h1>
|
|
<p className="text-[#888] font-body text-sm mt-1">
|
|
Use your Broadcast Beat account to post, reply, and vote.
|
|
</p>
|
|
</div>
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4"
|
|
>
|
|
{error && (
|
|
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<div>
|
|
<label className="block text-xs font-body uppercase tracking-wider text-[#888] mb-1.5">
|
|
Email
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => 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]"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-body uppercase tracking-wider text-[#888] mb-1.5">
|
|
Password
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
autoComplete="current-password"
|
|
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]"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors"
|
|
>
|
|
{loading ? 'Signing in…' : 'Sign In'}
|
|
</button>
|
|
<div className="text-center text-xs font-body text-[#888] pt-2">
|
|
No account yet?{' '}
|
|
<Link
|
|
href={`/forum/register?next=${encodeURIComponent(next)}`}
|
|
className="text-[#3b82f6] hover:underline"
|
|
>
|
|
Create one
|
|
</Link>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
<Footer />
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function ForumLoginPage() {
|
|
return (
|
|
<Suspense fallback={null}>
|
|
<ForumLoginInner />
|
|
</Suspense>
|
|
);
|
|
}
|