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

73
src/lib/forum/profile.ts Normal file
View File

@@ -0,0 +1,73 @@
import { createAdminClient } from "@/lib/supabase/admin";
interface AuthedUser {
id: string;
email?: string | null;
user_metadata?: { full_name?: string; avatar_url?: string } | null;
}
interface ForumProfile {
id: string;
user_id: string;
username: string;
display_name: string;
avatar_url: string | null;
bio: string | null;
role_title: string | null;
company: string | null;
}
function slugifyUsername(seed: string): string {
const base = seed
.toLowerCase()
.replace(/@.*$/, "")
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "")
.slice(0, 24);
return base || "user";
}
/**
* Get the forum profile for an auth user. If one doesn't exist yet (new
* signup), create it lazily with a username derived from email. Uses the
* service-role client to bypass the INSERT RLS gap on forum_user_profiles.
*/
export async function ensureForumProfile(user: AuthedUser): Promise<ForumProfile> {
const admin = createAdminClient();
const { data: existing } = await admin
.from("forum_user_profiles")
.select("id, user_id, username, display_name, avatar_url, bio, role_title, company")
.eq("user_id", user.id)
.maybeSingle();
if (existing) return existing as ForumProfile;
const base = slugifyUsername(user.email || user.user_metadata?.full_name || "user");
let username = base;
for (let i = 0; i < 5; i++) {
const { data: clash } = await admin
.from("forum_user_profiles")
.select("id")
.eq("username", username)
.maybeSingle();
if (!clash) break;
username = `${base}_${Math.floor(1000 + Math.random() * 9000)}`;
}
const displayName = user.user_metadata?.full_name?.trim() || base;
const { data: created, error } = await admin
.from("forum_user_profiles")
.insert({
user_id: user.id,
username,
display_name: displayName,
avatar_url: user.user_metadata?.avatar_url || null,
})
.select("id, user_id, username, display_name, avatar_url, bio, role_title, company")
.single();
if (error) throw new Error(`forum profile create failed: ${error.message}`);
return created as ForumProfile;
}