diff --git a/.replit b/.replit index 76d2fa09..c7bae709 100644 --- a/.replit +++ b/.replit @@ -52,6 +52,10 @@ externalPort = 80 localPort = 8044 externalPort = 3003 +[[ports]] +localPort = 37363 +externalPort = 3002 + [[ports]] localPort = 38557 externalPort = 3000 diff --git a/client/api/creators.ts b/client/api/creators.ts index 6206abb7..448c4dff 100644 --- a/client/api/creators.ts +++ b/client/api/creators.ts @@ -78,6 +78,7 @@ export async function getCreatorByUsername(username: string): Promise { } export async function createCreatorProfile(data: { + user_id: string; username: string; bio: string; skills: string[]; @@ -91,7 +92,10 @@ export async function createCreatorProfile(data: { headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); - if (!response.ok) throw new Error("Failed to create creator profile"); + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error?.error || "Failed to create creator profile"); + } return response.json(); } diff --git a/client/pages/Dashboard.tsx b/client/pages/Dashboard.tsx index db2c9176..89e13c36 100644 --- a/client/pages/Dashboard.tsx +++ b/client/pages/Dashboard.tsx @@ -161,6 +161,7 @@ export default function Dashboard() { const { user, profile, + session, loading: authLoading, signOut, profileComplete, @@ -209,18 +210,27 @@ export default function Dashboard() { }; const handleSaveRealm = async () => { - if (!user || !selectedRealm) return; + if (!user || !selectedRealm || !session?.access_token) return; setSavingRealm(true); try { - const { error } = await (window as any).supabaseClient - .from("user_profiles") - .update({ + const response = await fetch("/api/profile/update", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session.access_token}`, + }, + body: JSON.stringify({ + user_id: user.id, primary_realm: selectedRealm, experience_level: selectedExperience, - }) - .eq("id", user.id); + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Failed to save realm preference"); + } - if (error) throw error; aethexToast.success({ description: "Realm preference saved!", }); @@ -235,26 +245,36 @@ export default function Dashboard() { }; const handleSaveProfile = async () => { - if (!user) return; + if (!user || !session?.access_token) return; setSavingProfile(true); try { - const { error } = await (window as any).supabaseClient - .from("user_profiles") - .update({ + const response = await fetch("/api/profile/update", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session.access_token}`, + }, + body: JSON.stringify({ + user_id: user.id, full_name: displayName, bio: bio, website_url: website, linkedin_url: linkedin, github_url: github, twitter_url: twitter, - }) - .eq("id", user.id); + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Failed to update profile"); + } - if (error) throw error; aethexToast.success({ description: "Profile updated successfully!", }); } catch (error: any) { + console.error("Failed to update profile", error); aethexToast.error({ description: error?.message || "Failed to update profile", }); diff --git a/client/pages/creators/CreatorDirectory.tsx b/client/pages/creators/CreatorDirectory.tsx index 48c4e517..d21f6ffd 100644 --- a/client/pages/creators/CreatorDirectory.tsx +++ b/client/pages/creators/CreatorDirectory.tsx @@ -130,6 +130,7 @@ export default function CreatorDirectory() { setIsSubmitting(true); try { await createCreatorProfile({ + user_id: user.id, username: formData.username, bio: formData.bio, skills: formData.skills diff --git a/server/index.ts b/server/index.ts index 3c3e6e18..5d911648 100644 --- a/server/index.ts +++ b/server/index.ts @@ -342,13 +342,15 @@ export function createServer() { isProjectPassport: domain === "aethex.space", }; - console.log("[Subdomain] Detected:", { - hostname, - subdomain, - domain, - isCreatorPassport: domain === "aethex.me", - isProjectPassport: domain === "aethex.space", - }); + if (subdomain) { + console.log("[Subdomain] Detected:", { + hostname, + subdomain, + domain, + isCreatorPassport: domain === "aethex.me", + isProjectPassport: domain === "aethex.space", + }); + } next(); }); @@ -2705,6 +2707,80 @@ export function createServer() { } }); + // Profile update endpoint - used by Dashboard realm/settings + app.patch("/api/profile/update", async (req, res) => { + // Authenticate user via Bearer token + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ error: "Authentication required" }); + } + + const token = authHeader.replace("Bearer ", ""); + const { data: { user: authUser }, error: authError } = await adminSupabase.auth.getUser(token); + + if (authError || !authUser) { + return res.status(401).json({ error: "Invalid or expired auth token" }); + } + + const { user_id, ...updates } = req.body || {}; + + // Ensure user can only update their own profile + if (user_id && user_id !== authUser.id) { + return res.status(403).json({ error: "Cannot update another user's profile" }); + } + + const targetUserId = user_id || authUser.id; + + // Whitelist allowed fields for security + const allowedFields = [ + "full_name", + "bio", + "avatar_url", + "banner_url", + "location", + "website_url", + "github_url", + "linkedin_url", + "twitter_url", + "primary_realm", + "experience_level", + "user_type", + ]; + + const sanitizedUpdates: Record = {}; + for (const key of allowedFields) { + if (key in updates) { + sanitizedUpdates[key] = updates[key]; + } + } + + if (Object.keys(sanitizedUpdates).length === 0) { + return res.status(400).json({ error: "No valid fields to update" }); + } + + try { + const { data, error } = await adminSupabase + .from("user_profiles") + .update({ + ...sanitizedUpdates, + updated_at: new Date().toISOString(), + }) + .eq("id", targetUserId) + .select() + .single(); + + if (error) { + console.error("[Profile Update] Error:", error); + return res.status(500).json({ error: error.message }); + } + + return res.json(data); + } catch (e: any) { + console.error("[Profile Update] Exception:", e?.message); + return res.status(500).json({ error: e?.message || "Failed to update profile" }); + } + }); + // Wallet verification endpoint for Phase 2 Bridge UI app.post("/api/profile/wallet-verify", async (req, res) => { const { user_id, wallet_address } = req.body || {}; @@ -2988,10 +3064,10 @@ export function createServer() { badge_color: achievement.badge_color, xp_reward: achievement.xp_reward, }, - { onConflict: "id" }, + { onConflict: "id", ignoreDuplicates: true }, ); - if (error) { + if (error && error.code !== "23505") { console.error( `Failed to upsert achievement ${achievement.id}:`, error,