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
parent 4f55983095
commit f0e5f40100
5 changed files with 129 additions and 24 deletions

View file

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

View file

@ -78,6 +78,7 @@ export async function getCreatorByUsername(username: string): Promise<Creator> {
}
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();
}

View file

@ -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",
});

View file

@ -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

View file

@ -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<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
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,