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:
commit
cd838db84f
5 changed files with 129 additions and 24 deletions
4
.replit
4
.replit
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue