1- from collections import UserDict
21from typing import Annotated
32from datetime import timedelta
4- from unittest import result
53
6- from fastapi import APIRouter , Depends , HTTPException , status
4+ from fastapi import APIRouter , Depends , HTTPException , status , UploadFile
75from fastapi .security import OAuth2PasswordRequestForm
8- from sqlalchemy import select , func
6+ from pydantic import HttpUrl
7+ from sqlalchemy import exc , select , func
98from sqlalchemy .ext .asyncio import AsyncSession
109from sqlalchemy .orm import selectinload
10+ from PIL import UnidentifiedImageError
11+ from starlette .concurrency import run_in_threadpool
1112
1213from config import settings
1314import models
1415from database import get_db
1516from schemas import PostResponse , UserCreate , UserPrivate , UserPublic , UserUpdate , Token
16-
1717from auth import create_access_token , hash_password , verify_password , CurrentUser
18+ from image_utils import delete_profile_image , process_profile_image
1819
1920router = APIRouter ()
2021
@@ -165,8 +166,6 @@ async def update_user(
165166 user .username = user_update .username
166167 if user_update .email is not None :
167168 user .email = user_update .email .lower ()
168- if user_update .image_file is not None :
169- user .image_file = user_update .image_file
170169
171170 await db .commit ()
172171 await db .refresh (user )
@@ -192,5 +191,79 @@ async def delete_user(
192191 detail = "User not found" ,
193192 )
194193
194+ old_filename = user .image_file
195+
195196 await db .delete (user )
196- await db .commit ()
197+ await db .commit ()
198+
199+ if old_filename :
200+ delete_profile_image (old_filename )
201+
202+
203+ @router .patch ("/{user_id}/picture" , response_model = UserPrivate )
204+ async def upload_profile_picture (
205+ user_id : int ,
206+ file : UploadFile ,
207+ current_user : CurrentUser ,
208+ db : Annotated [AsyncSession , Depends (get_db )],
209+ ):
210+ if current_user .id != user_id :
211+ raise HTTPException (
212+ status_code = status .HTTP_403_FORBIDDEN ,
213+ detail = "Not authorized to update this user's picture" ,
214+ )
215+
216+ content = await file .read ()
217+
218+ if len (content ) > settings .max_upload_size_bytes :
219+ raise HTTPException (
220+ status_code = status .HTTP_400_BAD_REQUEST ,
221+ detail = f"File too large. Maximum size is { settings .max_upload_size_bytes // (1024 * 1024 )} MB" ,
222+ )
223+
224+ try :
225+ new_filename = await run_in_threadpool (process_profile_image , content )
226+ except UnidentifiedImageError as err :
227+ raise HTTPException (
228+ status_code = status .HTTP_400_BAD_REQUEST ,
229+ detail = "Invalid image file. Please upload a valid image (JPEG, PNG, GIF, WebP)." ,
230+ ) from err
231+
232+ old_filename = current_user .image_file
233+
234+ current_user .image_file = new_filename
235+ await db .commit ()
236+ await db .refresh (current_user )
237+
238+ if old_filename :
239+ delete_profile_image (old_filename )
240+
241+ return current_user
242+
243+ @router .delete ("/{user_id}/picture" , response_model = UserPrivate )
244+ async def delete_user_picture (
245+ user_id : int ,
246+ current_user : CurrentUser ,
247+ db : Annotated [AsyncSession , Depends (get_db )],
248+ ):
249+ if current_user .id != user_id :
250+ raise HTTPException (
251+ status_code = status .HTTP_403_FORBIDDEN ,
252+ detail = "Not authorized to delete this user's picture" ,
253+ )
254+
255+ old_filename = current_user .image_file
256+
257+ if old_filename is None :
258+ raise HTTPException (
259+ status_code = status .HTTP_400_BAD_REQUEST ,
260+ detail = "No profile picture to delete" ,
261+ )
262+
263+ current_user .image_file = None
264+ await db .commit ()
265+ await db .refresh (current_user )
266+
267+ delete_profile_image (old_filename )
268+
269+ return current_user
0 commit comments