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:
Ryan Salazar
2026-05-21 22:41:28 +00:00
parent 585a9503ab
commit a8b75593c2
11 changed files with 895 additions and 101 deletions

View File

@@ -194,8 +194,8 @@ export default function ForumThreadPage() {
// Reply form
const [replyBody, setReplyBody] = useState('');
const [replyAuthor, setReplyAuthor] = useState('');
const [submitting, setSubmitting] = useState(false);
const [replyError, setReplyError] = useState<string | null>(null);
// AI assistant
const [aiQuestion, setAiQuestion] = useState('');
@@ -230,6 +230,7 @@ export default function ForumThreadPage() {
e.preventDefault();
if (!replyBody.trim() || !threadId) return;
setSubmitting(true);
setReplyError(null);
try {
const res = await fetch('/api/forum/replies', {
method: 'POST',
@@ -237,18 +238,23 @@ export default function ForumThreadPage() {
body: JSON.stringify({
thread_id: threadId,
body: replyBody,
author_name: replyAuthor || '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 || `Reply failed (${res.status})`);
}
if (data.reply) {
setReplies(prev => [...prev, data.reply]);
setReplyBody('');
setReplyAuthor('');
setTimeout(() => repliesEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100);
}
} catch {
// silent
} catch (err: any) {
setReplyError(err.message || 'Failed to post reply. Please try again.');
} finally {
setSubmitting(false);
}
@@ -468,18 +474,11 @@ export default function ForumThreadPage() {
)}
</div>
{/* Reply Form */}
{thread.status !== 'closed' && (
{/* Reply Form — auth-gated */}
{thread.status !== 'closed' && isAuthenticated && (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-5">
<h3 className="text-white font-heading font-semibold mb-4">Post a Reply</h3>
<form onSubmit={handleReply} className="space-y-3">
<input
type="text"
placeholder="Your name (optional)"
value={replyAuthor}
onChange={e => setReplyAuthor(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]"
/>
<textarea
placeholder="Share your thoughts, experience, or answer..."
value={replyBody}
@@ -488,6 +487,11 @@ export default function ForumThreadPage() {
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"
/>
{replyError && (
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{replyError}
</div>
)}
<button
type="submit"
disabled={submitting || !replyBody.trim()}
@@ -499,6 +503,29 @@ export default function ForumThreadPage() {
</div>
)}
{thread.status !== 'closed' && !isAuthenticated && (
<div className="bg-[#0d1f35] border border-[#1e3a5f] rounded-lg p-5 text-center">
<h3 className="text-white font-heading font-semibold mb-1">Join the discussion</h3>
<p className="text-[#7a9ab8] font-body text-sm mb-4">
Sign in or create a free account to post replies.
</p>
<div className="flex items-center justify-center gap-3">
<Link
href={`/forum/login?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
>
Sign In
</Link>
<Link
href={`/forum/register?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
className="border border-[#3b82f6] text-[#3b82f6] hover:bg-[#3b82f6]/10 font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
>
Register
</Link>
</div>
</div>
)}
{thread.status === 'closed' && (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg p-4 text-center text-[#666] font-body text-sm">
This thread is closed to new replies.