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:
73
src/lib/forum/profile.ts
Normal file
73
src/lib/forum/profile.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user