Add authentication to profile updates and dashboard requests

Introduce Bearer token authentication for the /api/profile/update endpoint, ensuring users can only modify their own profiles. Update the Dashboard to include the authentication token in all API requests, enhancing security and data integrity.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 35bff579-2fa1-4c42-a661-d861f25fa2b6
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/AJbgVVq
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-04 09:19:42 +00:00
commit cd838db84f
5 changed files with 129 additions and 24 deletions

View file

@ -52,6 +52,10 @@ externalPort = 80
localPort = 8044 localPort = 8044
externalPort = 3003 externalPort = 3003
[[ports]]
localPort = 37363
externalPort = 3002
[[ports]] [[ports]]
localPort = 38557 localPort = 38557
externalPort = 3000 externalPort = 3000

View file

@ -78,6 +78,7 @@ export async function getCreatorByUsername(username: string): Promise<Creator> {
} }
export async function createCreatorProfile(data: { export async function createCreatorProfile(data: {
user_id: string;
username: string; username: string;
bio: string; bio: string;
skills: string[]; skills: string[];
@ -91,7 +92,10 @@ export async function createCreatorProfile(data: {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(data), 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(); return response.json();
} }

View file

@ -161,6 +161,7 @@ export default function Dashboard() {
const { const {
user, user,
profile, profile,
session,
loading: authLoading, loading: authLoading,
signOut, signOut,
profileComplete, profileComplete,
@ -209,18 +210,27 @@ export default function Dashboard() {
}; };
const handleSaveRealm = async () => { const handleSaveRealm = async () => {
if (!user || !selectedRealm) return; if (!user || !selectedRealm || !session?.access_token) return;
setSavingRealm(true); setSavingRealm(true);
try { try {
const { error } = await (window as any).supabaseClient const response = await fetch("/api/profile/update", {
.from("user_profiles") method: "PATCH",
.update({ headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session.access_token}`,
},
body: JSON.stringify({
user_id: user.id,
primary_realm: selectedRealm, primary_realm: selectedRealm,
experience_level: selectedExperience, 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({ aethexToast.success({
description: "Realm preference saved!", description: "Realm preference saved!",
}); });
@ -235,26 +245,36 @@ export default function Dashboard() {
}; };
const handleSaveProfile = async () => { const handleSaveProfile = async () => {
if (!user) return; if (!user || !session?.access_token) return;
setSavingProfile(true); setSavingProfile(true);
try { try {
const { error } = await (window as any).supabaseClient const response = await fetch("/api/profile/update", {
.from("user_profiles") method: "PATCH",
.update({ headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session.access_token}`,
},
body: JSON.stringify({
user_id: user.id,
full_name: displayName, full_name: displayName,
bio: bio, bio: bio,
website_url: website, website_url: website,
linkedin_url: linkedin, linkedin_url: linkedin,
github_url: github, github_url: github,
twitter_url: twitter, 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({ aethexToast.success({
description: "Profile updated successfully!", description: "Profile updated successfully!",
}); });
} catch (error: any) { } catch (error: any) {
console.error("Failed to update profile", error);
aethexToast.error({ aethexToast.error({
description: error?.message || "Failed to update profile", description: error?.message || "Failed to update profile",
}); });

View file

@ -130,6 +130,7 @@ export default function CreatorDirectory() {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await createCreatorProfile({ await createCreatorProfile({
user_id: user.id,
username: formData.username, username: formData.username,
bio: formData.bio, bio: formData.bio,
skills: formData.skills skills: formData.skills

View file

@ -342,13 +342,15 @@ export function createServer() {
isProjectPassport: domain === "aethex.space", isProjectPassport: domain === "aethex.space",
}; };
console.log("[Subdomain] Detected:", { if (subdomain) {
hostname, console.log("[Subdomain] Detected:", {
subdomain, hostname,
domain, subdomain,
isCreatorPassport: domain === "aethex.me", domain,
isProjectPassport: domain === "aethex.space", isCreatorPassport: domain === "aethex.me",
}); isProjectPassport: domain === "aethex.space",
});
}
next(); 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<string, any> = {};
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 // Wallet verification endpoint for Phase 2 Bridge UI
app.post("/api/profile/wallet-verify", async (req, res) => { app.post("/api/profile/wallet-verify", async (req, res) => {
const { user_id, wallet_address } = req.body || {}; const { user_id, wallet_address } = req.body || {};
@ -2988,10 +3064,10 @@ export function createServer() {
badge_color: achievement.badge_color, badge_color: achievement.badge_color,
xp_reward: achievement.xp_reward, xp_reward: achievement.xp_reward,
}, },
{ onConflict: "id" }, { onConflict: "id", ignoreDuplicates: true },
); );
if (error) { if (error && error.code !== "23505") {
console.error( console.error(
`Failed to upsert achievement ${achievement.id}:`, `Failed to upsert achievement ${achievement.id}:`,
error, error,