forum: require auth for posting + add login/register/edit pages
- 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>
This commit is contained in:
@@ -4,6 +4,7 @@ import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
|
||||
interface ForumCategory {
|
||||
id: string;
|
||||
@@ -173,8 +174,14 @@ export default function ForumCategoryPage() {
|
||||
const [showNewThread, setShowNewThread] = useState(false);
|
||||
const [newTitle, setNewTitle] = useState('');
|
||||
const [newBody, setNewBody] = useState('');
|
||||
const [newAuthor, setNewAuthor] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [newError, setNewError] = useState<string | null>(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = createClient();
|
||||
supabase.auth.getUser().then(({ data: { user } }) => setIsAuthenticated(!!user));
|
||||
}, []);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
@@ -228,6 +235,7 @@ export default function ForumCategoryPage() {
|
||||
e.preventDefault();
|
||||
if (!newTitle.trim() || !newBody.trim() || !category) return;
|
||||
setSubmitting(true);
|
||||
setNewError(null);
|
||||
try {
|
||||
const res = await fetch('/api/forum/threads', {
|
||||
method: 'POST',
|
||||
@@ -236,19 +244,24 @@ export default function ForumCategoryPage() {
|
||||
category_id: category.id,
|
||||
title: newTitle,
|
||||
body: newBody,
|
||||
author_name: newAuthor || 'Anonymous',
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
window.location.href = `/forum/login?next=${encodeURIComponent(window.location.pathname)}`;
|
||||
return;
|
||||
}
|
||||
throw new Error(data.error || `Failed (${res.status})`);
|
||||
}
|
||||
if (data.thread) {
|
||||
setThreads(prev => [data.thread, ...prev]);
|
||||
setNewTitle('');
|
||||
setNewBody('');
|
||||
setNewAuthor('');
|
||||
setShowNewThread(false);
|
||||
}
|
||||
} catch {
|
||||
// silent
|
||||
} catch (err: any) {
|
||||
setNewError(err.message || 'Failed to create thread. Please try again.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -282,29 +295,31 @@ export default function ForumCategoryPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowNewThread(!showNewThread)}
|
||||
className="flex-shrink-0 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
+ New Thread
|
||||
</button>
|
||||
{isAuthenticated ? (
|
||||
<button
|
||||
onClick={() => setShowNewThread(!showNewThread)}
|
||||
className="flex-shrink-0 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
+ New Thread
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/forum/login?next=${typeof window === 'undefined' ? '/forum' : encodeURIComponent(window.location.pathname)}`}
|
||||
className="flex-shrink-0 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
Sign in to post
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-container mx-auto px-4 py-6">
|
||||
{/* New Thread Form */}
|
||||
{showNewThread && (
|
||||
{/* New Thread Form — auth-gated */}
|
||||
{showNewThread && isAuthenticated && (
|
||||
<form onSubmit={handleNewThread} className="bg-[#1a2535] border border-[#2a3a50] rounded-lg p-5 mb-6">
|
||||
<h3 className="text-white font-heading font-semibold mb-4">Start a New Thread</h3>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Your name (optional)"
|
||||
value={newAuthor}
|
||||
onChange={e => setNewAuthor(e.target.value)}
|
||||
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Thread title *"
|
||||
@@ -321,6 +336,11 @@ export default function ForumCategoryPage() {
|
||||
rows={5}
|
||||
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#3b82f6] resize-none"
|
||||
/>
|
||||
{newError && (
|
||||
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
|
||||
{newError}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
Reference in New Issue
Block a user