Files
bacchus/apps/backend/app/api/profile.py
2025-09-28 19:13:01 +02:00

136 lines
4.3 KiB
Python

from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
from fastapi import status
from sqlalchemy.orm import Session as DBSession
from pathlib import Path
import shutil
from typing import Optional
from pydantic import BaseModel, EmailStr, field_validator
from app.core.database import SessionLocal
from app.core.auth import get_current_user
from app.models.user import User
router = APIRouter(prefix="/profile", tags=["profile"])
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# -----------------------------
# 1) Neuer PIN anfordern (Mock)
# -----------------------------
@router.post("/request-new-pin", status_code=status.HTTP_202_ACCEPTED)
def request_new_pin(current_user: User = Depends(get_current_user)):
return {"status": "queued", "message": "PIN request queued (mock)"}
# -----------------------------
# 2) Profil-Avatar hochladen (persistiert avatar_url)
# -----------------------------
UPLOAD_DIR = Path("media/avatars")
@router.post("/avatar", status_code=status.HTTP_200_OK)
def upload_avatar(
file: UploadFile = File(...),
db: DBSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
filename = file.filename or "avatar"
ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
if ext not in {"png", "jpg", "jpeg", "webp", "gif", ""}:
raise HTTPException(status_code=400, detail="Unsupported file type")
final_ext = ext if ext else "png"
dest = UPLOAD_DIR / f"{current_user.id}.{final_ext}"
with dest.open("wb") as out:
shutil.copyfileobj(file.file, out)
# User in diese Session laden
u = db.query(User).filter(User.id == current_user.id).first()
if not u:
raise HTTPException(status_code=404, detail="User not found")
u.avatar_url = f"/media/avatars/{u.id}.{final_ext}"
db.commit()
db.refresh(u)
import time
return {
"status": "ok",
"avatar_url": f"{u.avatar_url}?t={int(time.time())}",
"path": str(dest),
}
# -----------------------------
# 3) Eigene Basisdaten ändern (inkl. PayPal + Passwort)
# -----------------------------
class ProfileUpdate(BaseModel):
alias: Optional[str] = None
paypal_email: Optional[EmailStr] = None
visible_in_stats: Optional[bool] = None
public_stats: Optional[bool] = None
# Passwortwechsel
current_password: Optional[str] = None
new_password: Optional[str] = None
@field_validator("new_password")
@classmethod
def _min_len_pw(cls, v):
if v is not None and len(v) < 8:
raise ValueError("Password too short (min. 8)")
return v
@router.put("/me", response_model=dict)
def update_profile(
payload: ProfileUpdate,
db: DBSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
u = db.query(User).filter(User.id == current_user.id).first()
if not u:
raise HTTPException(status_code=404, detail="User not found")
changed = False
# Alias
if payload.alias is not None:
u.alias = payload.alias
changed = True
# PayPal-Mail
if payload.paypal_email is not None:
u.paypal_email = str(payload.paypal_email).lower()
changed = True
# Sichtbarkeit (Kompatibilität: beide Keys akzeptieren)
flag = payload.public_stats if payload.public_stats is not None else payload.visible_in_stats
if flag is not None:
u.public_stats = bool(flag)
changed = True
# Passwortwechsel nur mit current_password
if payload.new_password is not None:
from passlib.context import CryptContext
pwd = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Wenn ein Passwort gesetzt ist, muss current_password mitgeschickt werden
if getattr(u, "hashed_password", None):
if not payload.current_password:
raise HTTPException(status_code=400, detail="Current password required")
if not pwd.verify(payload.current_password, u.hashed_password):
raise HTTPException(status_code=400, detail="Current password invalid")
u.hashed_password = pwd.hash(payload.new_password)
changed = True
if changed:
db.commit()
db.refresh(u)
return {"status": "ok"}