Skip to content

Commit 36fa42b

Browse files
🎨 Auto format and update with pre-commit
1 parent 68d3ad2 commit 36fa42b

File tree

7 files changed

+140
-58
lines changed

7 files changed

+140
-58
lines changed

backend/app/api/routes/users.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import uuid
21
import shutil
2+
import uuid
33
from pathlib import Path
44
from typing import Any
55

6-
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
6+
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
77
from sqlmodel import col, delete, func, select
88

99
from app import crud
@@ -103,26 +103,23 @@ def update_user_me(
103103

104104
@router.post("/me/avatar", response_model=UserPublic)
105105
def update_user_avatar(
106-
*,
107-
session: SessionDep,
108-
current_user: CurrentUser,
109-
file: UploadFile = File(...)
106+
*, session: SessionDep, current_user: CurrentUser, file: UploadFile = File(...)
110107
) -> Any:
111108
"""
112109
Upload user avatar.
113110
"""
114111
# Ensure upload directory exists
115112
upload_dir = Path("app/static/uploads")
116113
upload_dir.mkdir(parents=True, exist_ok=True)
117-
114+
118115
# Generate unique filename
119116
file_ext = Path(file.filename).suffix if file.filename else ""
120117
file_name = f"{current_user.id}_{uuid.uuid4()}{file_ext}"
121118
file_path = upload_dir / file_name
122-
119+
123120
with file_path.open("wb") as buffer:
124121
shutil.copyfileobj(file.file, buffer)
125-
122+
126123
# Update user profile
127124
# Assuming served at /static/uploads/
128125
avatar_url = f"/static/uploads/{file_name}"

backend/app/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import sentry_sdk
21
import os
2+
3+
import sentry_sdk
34
from fastapi import FastAPI
4-
from fastapi.staticfiles import StaticFiles
55
from fastapi.routing import APIRoute
6+
from fastapi.staticfiles import StaticFiles
67
from starlette.middleware.cors import CORSMiddleware
78

89
from app.api.main import api_router

compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ services:
115115
interval: 10s
116116
timeout: 5s
117117
retries: 5
118-
118+
119119
volumes:
120120
- app-static-data:/app/backend/app/static
121121

frontend/src/client/schemas.gen.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ export const Body_login_login_access_tokenSchema = {
5757
title: 'Body_login-login_access_token'
5858
} as const;
5959

60+
export const Body_users_update_user_avatarSchema = {
61+
properties: {
62+
file: {
63+
type: 'string',
64+
format: 'binary',
65+
title: 'File'
66+
}
67+
},
68+
type: 'object',
69+
required: ['file'],
70+
title: 'Body_users-update_user_avatar'
71+
} as const;
72+
6073
export const HTTPValidationErrorSchema = {
6174
properties: {
6275
detail: {
@@ -318,6 +331,18 @@ export const UserCreateSchema = {
318331
],
319332
title: 'Full Name'
320333
},
334+
avatar_url: {
335+
anyOf: [
336+
{
337+
type: 'string',
338+
maxLength: 500
339+
},
340+
{
341+
type: 'null'
342+
}
343+
],
344+
title: 'Avatar Url'
345+
},
321346
password: {
322347
type: 'string',
323348
maxLength: 128,
@@ -360,6 +385,18 @@ export const UserPublicSchema = {
360385
],
361386
title: 'Full Name'
362387
},
388+
avatar_url: {
389+
anyOf: [
390+
{
391+
type: 'string',
392+
maxLength: 500
393+
},
394+
{
395+
type: 'null'
396+
}
397+
],
398+
title: 'Avatar Url'
399+
},
363400
id: {
364401
type: 'string',
365402
format: 'uuid',
@@ -452,6 +489,18 @@ export const UserUpdateSchema = {
452489
],
453490
title: 'Full Name'
454491
},
492+
avatar_url: {
493+
anyOf: [
494+
{
495+
type: 'string',
496+
maxLength: 500
497+
},
498+
{
499+
type: 'null'
500+
}
501+
],
502+
title: 'Avatar Url'
503+
},
455504
password: {
456505
anyOf: [
457506
{

frontend/src/client/sdk.gen.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { CancelablePromise } from './core/CancelablePromise';
44
import { OpenAPI } from './core/OpenAPI';
55
import { request as __request } from './core/request';
6-
import type { ItemsReadItemsData, ItemsReadItemsResponse, ItemsCreateItemData, ItemsCreateItemResponse, ItemsReadItemData, ItemsReadItemResponse, ItemsUpdateItemData, ItemsUpdateItemResponse, ItemsDeleteItemData, ItemsDeleteItemResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, PrivateCreateUserData, PrivateCreateUserResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen';
6+
import type { ItemsReadItemsData, ItemsReadItemsResponse, ItemsCreateItemData, ItemsCreateItemResponse, ItemsReadItemData, ItemsReadItemResponse, ItemsUpdateItemData, ItemsUpdateItemResponse, ItemsDeleteItemData, ItemsDeleteItemResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, PrivateCreateUserData, PrivateCreateUserResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdateUserAvatarData, UsersUpdateUserAvatarResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen';
77

88
export class ItemsService {
99
/**
@@ -325,6 +325,26 @@ export class UsersService {
325325
});
326326
}
327327

328+
/**
329+
* Update User Avatar
330+
* Upload user avatar.
331+
* @param data The data for the request.
332+
* @param data.formData
333+
* @returns UserPublic Successful Response
334+
* @throws ApiError
335+
*/
336+
public static updateUserAvatar(data: UsersUpdateUserAvatarData): CancelablePromise<UsersUpdateUserAvatarResponse> {
337+
return __request(OpenAPI, {
338+
method: 'POST',
339+
url: '/api/v1/users/me/avatar',
340+
formData: data.formData,
341+
mediaType: 'multipart/form-data',
342+
errors: {
343+
422: 'Validation Error'
344+
}
345+
});
346+
}
347+
328348
/**
329349
* Update Password Me
330350
* Update own password.

frontend/src/client/types.gen.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export type Body_login_login_access_token = {
99
client_secret?: (string | null);
1010
};
1111

12+
export type Body_users_update_user_avatar = {
13+
file: (Blob | File);
14+
};
15+
1216
export type HTTPValidationError = {
1317
detail?: Array<ValidationError>;
1418
};
@@ -67,6 +71,7 @@ export type UserCreate = {
6771
is_active?: boolean;
6872
is_superuser?: boolean;
6973
full_name?: (string | null);
74+
avatar_url?: (string | null);
7075
password: string;
7176
};
7277

@@ -96,6 +101,7 @@ export type UserUpdate = {
96101
is_active?: boolean;
97102
is_superuser?: boolean;
98103
full_name?: (string | null);
104+
avatar_url?: (string | null);
99105
password?: (string | null);
100106
};
101107

@@ -197,6 +203,12 @@ export type UsersUpdateUserMeData = {
197203

198204
export type UsersUpdateUserMeResponse = (UserPublic);
199205

206+
export type UsersUpdateUserAvatarData = {
207+
formData: Body_users_update_user_avatar;
208+
};
209+
210+
export type UsersUpdateUserAvatarResponse = (UserPublic);
211+
200212
export type UsersUpdatePasswordMeData = {
201213
requestBody: UpdatePassword;
202214
};

frontend/src/components/UserSettings/UserInformation.tsx

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { zodResolver } from "@hookform/resolvers/zod"
22
import { useMutation, useQueryClient } from "@tanstack/react-query"
3-
import { useState, useRef, type ChangeEvent } from "react"
3+
import { type ChangeEvent, useRef, useState } from "react"
44
import { useForm } from "react-hook-form"
55
import { z } from "zod"
66

@@ -48,23 +48,26 @@ const UserInformation = () => {
4848
formData.append("file", file)
4949

5050
try {
51-
const token = localStorage.getItem("access_token")
52-
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/v1/users/me/avatar`, {
53-
method: "POST",
54-
headers: {
55-
Authorization: `Bearer ${token}`,
56-
},
57-
body: formData,
58-
})
59-
60-
if (!response.ok) {
61-
throw new Error("Failed to upload avatar")
62-
}
63-
64-
showSuccessToast("Avatar updated successfully")
65-
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
66-
} catch (error) {
67-
showErrorToast("Error uploading avatar")
51+
const token = localStorage.getItem("access_token")
52+
const response = await fetch(
53+
`${import.meta.env.VITE_API_URL}/api/v1/users/me/avatar`,
54+
{
55+
method: "POST",
56+
headers: {
57+
Authorization: `Bearer ${token}`,
58+
},
59+
body: formData,
60+
},
61+
)
62+
63+
if (!response.ok) {
64+
throw new Error("Failed to upload avatar")
65+
}
66+
67+
showSuccessToast("Avatar updated successfully")
68+
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
69+
} catch (_error) {
70+
showErrorToast("Error uploading avatar")
6871
}
6972
}
7073

@@ -117,35 +120,35 @@ const UserInformation = () => {
117120
return (
118121
<div className="max-w-md">
119122
<h3 className="text-lg font-semibold py-4">User Information</h3>
120-
123+
121124
<div className="flex items-center gap-4 mb-6">
122-
<div
123-
className="relative w-24 h-24 rounded-full overflow-hidden bg-gray-100 border border-gray-200"
124-
>
125-
{currentUser?.avatar_url ? (
126-
<img
127-
src={`${import.meta.env.VITE_API_URL}${currentUser.avatar_url}`}
128-
alt="Avatar"
129-
className="w-full h-full object-cover"
130-
/>
131-
) : (
132-
<div className="flex items-center justify-center h-full text-3xl font-bold text-gray-300 uppercase">
133-
{currentUser?.full_name?.charAt(0) || currentUser?.email?.charAt(0) || "?"}
134-
</div>
135-
)}
136-
</div>
137-
<div className="flex flex-col gap-2">
138-
<Button variant="outline" size="sm" onClick={handleAvatarClick}>
139-
Change Avatar
140-
</Button>
141-
<input
142-
type="file"
143-
ref={fileInputRef}
144-
className="hidden"
145-
accept="image/*"
146-
onChange={handleFileChange}
125+
<div className="relative w-24 h-24 rounded-full overflow-hidden bg-gray-100 border border-gray-200">
126+
{currentUser?.avatar_url ? (
127+
<img
128+
src={`${import.meta.env.VITE_API_URL}${currentUser.avatar_url}`}
129+
alt="Avatar"
130+
className="w-full h-full object-cover"
147131
/>
148-
</div>
132+
) : (
133+
<div className="flex items-center justify-center h-full text-3xl font-bold text-gray-300 uppercase">
134+
{currentUser?.full_name?.charAt(0) ||
135+
currentUser?.email?.charAt(0) ||
136+
"?"}
137+
</div>
138+
)}
139+
</div>
140+
<div className="flex flex-col gap-2">
141+
<Button variant="outline" size="sm" onClick={handleAvatarClick}>
142+
Change Avatar
143+
</Button>
144+
<input
145+
type="file"
146+
ref={fileInputRef}
147+
className="hidden"
148+
accept="image/*"
149+
onChange={handleFileChange}
150+
/>
151+
</div>
149152
</div>
150153

151154
<Form {...form}>

0 commit comments

Comments
 (0)