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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user