new init
This commit is contained in:
135
apps/backend/app/api/profile.py
Normal file
135
apps/backend/app/api/profile.py
Normal file
@@ -0,0 +1,135 @@
|
||||
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"}
|
Reference in New Issue
Block a user