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:
parent
4f55983095
commit
f0e5f40100
5 changed files with 129 additions and 24 deletions
4
.replit
4
.replit
|
|
@ -52,6 +52,10 @@ externalPort = 80
|
|||
localPort = 8044
|
||||
externalPort = 3003
|
||||
|
||||
[[ports]]
|
||||
localPort = 37363
|
||||
externalPort = 3002
|
||||
|
||||
[[ports]]
|
||||
localPort = 38557
|
||||
externalPort = 3000
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue