From cd6fabb31c31ab869f809b9020df6c2e80705f47 Mon Sep 17 00:00:00 2001 From: Mistral Vibe Date: Mon, 30 Mar 2026 20:17:21 +0200 Subject: [PATCH] fix: correct avatar upload and DiceBear URL version - Add api.upload() to client.ts that passes FormData without setting Content-Type, letting the browser set multipart/form-data with the correct boundary (was causing 422 on the upload endpoint) - Use api.upload() instead of api.post() for avatar file upload - Update DiceBear URLs from v6 to 9.x in both frontend and backend Co-Authored-By: Claude Sonnet 4.6 --- api/src/rehearsalhub/services/avatar.py | 2 +- web/src/api/client.ts | 9 +++++++-- web/src/pages/SettingsPage.tsx | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/api/src/rehearsalhub/services/avatar.py b/api/src/rehearsalhub/services/avatar.py index 7decc03..2e4780e 100644 --- a/api/src/rehearsalhub/services/avatar.py +++ b/api/src/rehearsalhub/services/avatar.py @@ -9,7 +9,7 @@ class AvatarService: """Service for generating and managing user avatars.""" def __init__(self): - self.base_url = "https://api.dicebear.com/v6" + self.base_url = "https://api.dicebear.com/9.x" async def generate_avatar_url(self, seed: str, style: str = "identicon") -> str: """Generate a DiceBear avatar URL for the given seed. diff --git a/web/src/api/client.ts b/web/src/api/client.ts index bacb488..6914219 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -14,13 +14,16 @@ export function clearToken(): void { async function request( path: string, - options: RequestInit = {} + options: RequestInit = {}, + isFormData = false ): Promise { const token = getToken(); const headers: Record = { - "Content-Type": "application/json", ...(options.headers as Record), }; + if (!isFormData) { + headers["Content-Type"] = "application/json"; + } if (token) { headers["Authorization"] = `Bearer ${token}`; } @@ -42,6 +45,8 @@ export const api = { get: (path: string) => request(path), post: (path: string, body: unknown) => request(path, { method: "POST", body: JSON.stringify(body) }), + upload: (path: string, formData: FormData) => + request(path, { method: "POST", body: formData }, true), patch: (path: string, body: unknown) => request(path, { method: "PATCH", body: JSON.stringify(body) }), delete: (path: string) => request(path, { method: "DELETE" }), diff --git a/web/src/pages/SettingsPage.tsx b/web/src/pages/SettingsPage.tsx index 865eacb..af0a7ea 100644 --- a/web/src/pages/SettingsPage.tsx +++ b/web/src/pages/SettingsPage.tsx @@ -211,7 +211,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) { console.log("Uploading file to /auth/me/avatar"); console.log("Final file size:", processedFile.size); - const response = await api.post('/auth/me/avatar', formData); + const response = await api.upload('/auth/me/avatar', formData); console.log("Upload response:", response); setAvatarUrl(response.avatar_url || ''); @@ -263,7 +263,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) { try { // Generate a new random avatar using user ID as seed for consistency const seed = Math.random().toString(36).substring(2, 15); - const newAvatarUrl = `https://api.dicebear.com/v6/identicon/svg?seed=${seed}&backgroundType=gradientLinear&size=128`; + const newAvatarUrl = `https://api.dicebear.com/9.x/identicon/svg?seed=${seed}&backgroundType=gradientLinear&size=128`; console.log("Generated avatar URL:", newAvatarUrl); console.log("Calling updateSettings with:", { avatar_url: newAvatarUrl }); @@ -300,7 +300,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) { onError={() => { console.error("Failed to load avatar:", avatarUrl); // Set to default avatar on error - setAvatarUrl(`https://api.dicebear.com/v6/identicon/svg?seed=${me.id}&backgroundType=gradientLinear&size=128`); + setAvatarUrl(`https://api.dicebear.com/9.x/identicon/svg?seed=${me.id}&backgroundType=gradientLinear&size=128`); }} />