From e6f685df859899330eb8fc703844598e7d1ac8fc Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 15 Nov 2025 16:37:55 +0000 Subject: [PATCH] Create API endpoint to save profile updates cgen-1527d4c3e77a4b4791fb5efc924554cb --- api/user/arm-affiliations.ts | 106 +++++++++++++++++++++++++++++++++++ api/user/profile-update.ts | 93 ++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 api/user/arm-affiliations.ts create mode 100644 api/user/profile-update.ts diff --git a/api/user/arm-affiliations.ts b/api/user/arm-affiliations.ts new file mode 100644 index 00000000..828f3da8 --- /dev/null +++ b/api/user/arm-affiliations.ts @@ -0,0 +1,106 @@ +import { supabase } from "../_supabase"; + +const VALID_ARMS = ["foundation", "gameforge", "labs", "corp", "devlink"]; +const VALID_TYPES = ["courses", "projects", "research", "opportunities", "manual"]; + +export default async (req: Request) => { + try { + const authHeader = req.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return new Response("Unauthorized", { status: 401 }); + } + + const token = authHeader.slice(7); + const { data: userData } = await supabase.auth.getUser(token); + + if (!userData.user) { + return new Response("Unauthorized", { status: 401 }); + } + + const userId = userData.user.id; + + // GET - Fetch user's arm affiliations + if (req.method === "GET") { + const { data, error } = await supabase + .from("user_arm_affiliations") + .select("*") + .eq("user_id", userId) + .order("created_at", { ascending: false }); + + if (error) { + return new Response(JSON.stringify({ error: error.message }), { status: 500 }); + } + + return new Response(JSON.stringify(data), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // POST - Add or confirm arm affiliation + if (req.method === "POST") { + const body = await req.json(); + const { arm, affiliation_type, affiliation_data, confirmed } = body; + + if (!VALID_ARMS.includes(arm) || !VALID_TYPES.includes(affiliation_type)) { + return new Response("Invalid arm or affiliation type", { status: 400 }); + } + + // Upsert to handle duplicates + const { data, error } = await supabase + .from("user_arm_affiliations") + .upsert( + { + user_id: userId, + arm, + affiliation_type, + affiliation_data: affiliation_data || {}, + confirmed: confirmed === true, + }, + { onConflict: "user_id,arm,affiliation_type" } + ) + .select() + .single(); + + if (error) { + return new Response(JSON.stringify({ error: error.message }), { status: 500 }); + } + + return new Response(JSON.stringify(data), { + status: 201, + headers: { "Content-Type": "application/json" }, + }); + } + + // DELETE - Remove arm affiliation + if (req.method === "DELETE") { + const body = await req.json(); + const { arm, affiliation_type } = body; + + if (!VALID_ARMS.includes(arm)) { + return new Response("Invalid arm", { status: 400 }); + } + + const { error } = await supabase + .from("user_arm_affiliations") + .delete() + .eq("user_id", userId) + .eq("arm", arm) + .eq("affiliation_type", affiliation_type || null); + + if (error) { + return new Response(JSON.stringify({ error: error.message }), { status: 500 }); + } + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response("Method not allowed", { status: 405 }); + } catch (error: any) { + console.error("Arm affiliations error:", error); + return new Response(JSON.stringify({ error: error.message }), { status: 500 }); + } +}; diff --git a/api/user/profile-update.ts b/api/user/profile-update.ts new file mode 100644 index 00000000..95ff48f7 --- /dev/null +++ b/api/user/profile-update.ts @@ -0,0 +1,93 @@ +import { supabase } from "../_supabase"; + +export default async (req: Request) => { + if (req.method !== "PUT" && req.method !== "POST") { + return new Response("Method not allowed", { status: 405 }); + } + + try { + const authHeader = req.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return new Response("Unauthorized", { status: 401 }); + } + + const token = authHeader.slice(7); + const { data: userData } = await supabase.auth.getUser(token); + + if (!userData.user) { + return new Response("Unauthorized", { status: 401 }); + } + + const body = await req.json(); + const userId = userData.user.id; + + // Sanitize and validate input + const updates: any = {}; + + // Profile fields + if ("bio_detailed" in body) updates.bio_detailed = body.bio_detailed || null; + if ("twitter_url" in body) updates.twitter_url = body.twitter_url || null; + if ("linkedin_url" in body) updates.linkedin_url = body.linkedin_url || null; + if ("github_url" in body) updates.github_url = body.github_url || null; + if ("portfolio_url" in body) updates.portfolio_url = body.portfolio_url || null; + if ("youtube_url" in body) updates.youtube_url = body.youtube_url || null; + if ("twitch_url" in body) updates.twitch_url = body.twitch_url || null; + + // Professional info + if ("hourly_rate" in body) + updates.hourly_rate = body.hourly_rate ? parseFloat(body.hourly_rate) : null; + if ("availability_status" in body) + updates.availability_status = + ["available", "limited", "unavailable"].includes(body.availability_status) ? + body.availability_status : "available"; + if ("timezone" in body) updates.timezone = body.timezone || null; + if ("location" in body) updates.location = body.location || null; + + // Arrays + if ("languages" in body) { + updates.languages = Array.isArray(body.languages) ? body.languages : []; + } + if ("skills_detailed" in body) { + updates.skills_detailed = Array.isArray(body.skills_detailed) ? body.skills_detailed : []; + } + if ("work_experience" in body) { + updates.work_experience = Array.isArray(body.work_experience) ? body.work_experience : []; + } + if ("portfolio_items" in body) { + updates.portfolio_items = Array.isArray(body.portfolio_items) ? body.portfolio_items : []; + } + if ("arm_affiliations" in body) { + const validArms = ["foundation", "gameforge", "labs", "corp", "devlink"]; + updates.arm_affiliations = Array.isArray(body.arm_affiliations) ? + body.arm_affiliations.filter((a: string) => validArms.includes(a)) : []; + } + + // Nexus specific + if ("nexus_profile_complete" in body) updates.nexus_profile_complete = body.nexus_profile_complete === true; + if ("nexus_headline" in body) updates.nexus_headline = body.nexus_headline || null; + if ("nexus_categories" in body) { + updates.nexus_categories = Array.isArray(body.nexus_categories) ? body.nexus_categories : []; + } + + // Update the profile + const { data, error } = await supabase + .from("user_profiles") + .update(updates) + .eq("id", userId) + .select() + .single(); + + if (error) { + console.error("Profile update error:", error); + return new Response(JSON.stringify({ error: error.message }), { status: 500 }); + } + + return new Response(JSON.stringify(data), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error: any) { + console.error("Profile update error:", error); + return new Response(JSON.stringify({ error: error.message }), { status: 500 }); + } +};