mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 17:13:42 +00:00
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:
parent
1fa5d20d40
commit
661f1ce552
4 changed files with 82 additions and 1 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -45,3 +45,7 @@ Thumbs.db
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Uploaded avatars (keep directory structure only)
|
||||||
|
backend/uploads/avatars/*
|
||||||
|
!backend/uploads/avatars/.gitkeep
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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__))),
|
||||||
|
|
|
||||||
0
backend/uploads/avatars/.gitkeep
Normal file
0
backend/uploads/avatars/.gitkeep
Normal file
Loading…
Reference in a new issue