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 <noreply@anthropic.com>
This commit is contained in:
Mistral Vibe
2026-03-30 20:17:21 +02:00
parent da051be673
commit cd6fabb31c
3 changed files with 11 additions and 6 deletions

View File

@@ -9,7 +9,7 @@ class AvatarService:
"""Service for generating and managing user avatars.""" """Service for generating and managing user avatars."""
def __init__(self): 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: async def generate_avatar_url(self, seed: str, style: str = "identicon") -> str:
"""Generate a DiceBear avatar URL for the given seed. """Generate a DiceBear avatar URL for the given seed.

View File

@@ -14,13 +14,16 @@ export function clearToken(): void {
async function request<T>( async function request<T>(
path: string, path: string,
options: RequestInit = {} options: RequestInit = {},
isFormData = false
): Promise<T> { ): Promise<T> {
const token = getToken(); const token = getToken();
const headers: Record<string, string> = { const headers: Record<string, string> = {
"Content-Type": "application/json",
...(options.headers as Record<string, string>), ...(options.headers as Record<string, string>),
}; };
if (!isFormData) {
headers["Content-Type"] = "application/json";
}
if (token) { if (token) {
headers["Authorization"] = `Bearer ${token}`; headers["Authorization"] = `Bearer ${token}`;
} }
@@ -42,6 +45,8 @@ export const api = {
get: <T>(path: string) => request<T>(path), get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body: unknown) => post: <T>(path: string, body: unknown) =>
request<T>(path, { method: "POST", body: JSON.stringify(body) }), request<T>(path, { method: "POST", body: JSON.stringify(body) }),
upload: <T>(path: string, formData: FormData) =>
request<T>(path, { method: "POST", body: formData }, true),
patch: <T>(path: string, body: unknown) => patch: <T>(path: string, body: unknown) =>
request<T>(path, { method: "PATCH", body: JSON.stringify(body) }), request<T>(path, { method: "PATCH", body: JSON.stringify(body) }),
delete: (path: string) => request<void>(path, { method: "DELETE" }), delete: (path: string) => request<void>(path, { method: "DELETE" }),

View File

@@ -211,7 +211,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
console.log("Uploading file to /auth/me/avatar"); console.log("Uploading file to /auth/me/avatar");
console.log("Final file size:", processedFile.size); console.log("Final file size:", processedFile.size);
const response = await api.post<MemberRead>('/auth/me/avatar', formData); const response = await api.upload<MemberRead>('/auth/me/avatar', formData);
console.log("Upload response:", response); console.log("Upload response:", response);
setAvatarUrl(response.avatar_url || ''); setAvatarUrl(response.avatar_url || '');
@@ -263,7 +263,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
try { try {
// Generate a new random avatar using user ID as seed for consistency // Generate a new random avatar using user ID as seed for consistency
const seed = Math.random().toString(36).substring(2, 15); 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("Generated avatar URL:", newAvatarUrl);
console.log("Calling updateSettings with:", { avatar_url: newAvatarUrl }); console.log("Calling updateSettings with:", { avatar_url: newAvatarUrl });
@@ -300,7 +300,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
onError={() => { onError={() => {
console.error("Failed to load avatar:", avatarUrl); console.error("Failed to load avatar:", avatarUrl);
// Set to default avatar on error // 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`);
}} }}
/> />
<button <button