feat: add avatar upload/delete endpoints with static file serving

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-26 09:39:21 +00:00
parent 1fa5d20d40
commit 661f1ce552
4 changed files with 82 additions and 1 deletions

4
.gitignore vendored
View file

@ -45,3 +45,7 @@ Thumbs.db
# Logs # Logs
*.log *.log
# Uploaded avatars (keep directory structure only)
backend/uploads/avatars/*
!backend/uploads/avatars/.gitkeep

View file

@ -1,6 +1,9 @@
"""Auth API router: login, register, refresh, logout, MFA, password change.""" """Auth API router: login, register, refresh, logout, MFA, password change."""
from fastapi import APIRouter, Depends, Request import os
import time
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.dependencies import get_current_user from app.core.dependencies import get_current_user
@ -163,3 +166,72 @@ def update_profile_endpoint(
update_data = data.model_dump(exclude_unset=True) update_data = data.model_dump(exclude_unset=True)
user = update_profile(db, current_user, **update_data) user = update_profile(db, current_user, **update_data)
return UserResponse.model_validate(user) return UserResponse.model_validate(user)
# ---------------------------------------------------------------------------
# Avatar upload / delete
# ---------------------------------------------------------------------------
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png"}
MAX_AVATAR_SIZE = 2 * 1024 * 1024 # 2MB
@router.post("/avatar", response_model=UserResponse)
async def upload_avatar(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Upload or replace the authenticated user's avatar image."""
if file.content_type not in ALLOWED_IMAGE_TYPES:
raise HTTPException(status_code=400, detail="Only JPG and PNG images are allowed")
contents = await file.read()
if len(contents) > MAX_AVATAR_SIZE:
raise HTTPException(status_code=400, detail="Image must be smaller than 2MB")
ext = "jpg" if file.content_type == "image/jpeg" else "png"
filename = f"{current_user.id}_{int(time.time())}.{ext}"
uploads_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
"uploads", "avatars",
)
os.makedirs(uploads_dir, exist_ok=True)
# Delete old avatar file if exists
if current_user.avatar_url:
old_file = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
current_user.avatar_url.lstrip("/"),
)
if os.path.isfile(old_file):
os.remove(old_file)
filepath = os.path.join(uploads_dir, filename)
with open(filepath, "wb") as f:
f.write(contents)
current_user.avatar_url = f"/uploads/avatars/{filename}"
db.commit()
db.refresh(current_user)
return UserResponse.model_validate(current_user)
@router.delete("/avatar", response_model=UserResponse)
def delete_avatar(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Remove the authenticated user's avatar image."""
if current_user.avatar_url:
old_file = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
current_user.avatar_url.lstrip("/"),
)
if os.path.isfile(old_file):
os.remove(old_file)
current_user.avatar_url = None
db.commit()
db.refresh(current_user)
return UserResponse.model_validate(current_user)

View file

@ -42,6 +42,11 @@ def health_check():
return {"status": "ok"} return {"status": "ok"}
# --- Serve uploaded files ---
_uploads_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "uploads")
if os.path.isdir(_uploads_dir):
app.mount("/uploads", StaticFiles(directory=_uploads_dir), name="uploads")
# --- Serve frontend SPA --- # --- Serve frontend SPA ---
_frontend_dist = os.path.join( _frontend_dist = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), os.path.dirname(os.path.dirname(os.path.dirname(__file__))),

View file