- worlds-settings Zahnrad komplett entfernt (war auf Mobile sichtbar, auf Desktop schon hidden) - exp-fab: bottom jetzt calc(--nav-bottom-height + --safe-bottom + --space-2) — kein Overlap mit worlds-back auf iPhone - Karte POI: neue Typen bank, bank_kotbeutel, bank_kotbeutel_abfall, kotbeutel_abfall (Backend + Frontend) - Welten-Chip-Config: GET/PUT /profile/world-config, Spalte users.world_config TEXT (Migration), Sync bei Init + Speichern
140 lines
4.7 KiB
Python
140 lines
4.7 KiB
Python
"""BAN YARO — User-Profil Routes"""
|
|
|
|
import io
|
|
import os
|
|
import uuid
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
|
from pydantic import BaseModel
|
|
|
|
from auth import get_current_user
|
|
from database import db
|
|
|
|
router = APIRouter()
|
|
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
|
|
|
VALID_ERFAHRUNG = {"einsteiger", "erfahren", "trainer", "zuechter"}
|
|
VALID_SICHTBARKEIT = {"public", "friends", "private"}
|
|
|
|
|
|
class ProfileUpdate(BaseModel):
|
|
real_name: Optional[str] = None
|
|
bio: Optional[str] = None
|
|
wohnort: Optional[str] = None
|
|
erfahrung: Optional[str] = None
|
|
social_link: Optional[str] = None
|
|
profil_sichtbarkeit: Optional[str] = None
|
|
notes_ki_enabled: Optional[int] = None
|
|
|
|
|
|
def _load_user(user_id: int) -> dict:
|
|
with db() as conn:
|
|
row = conn.execute(
|
|
"""SELECT id, name, real_name, email, rolle, is_premium, email_verified,
|
|
bio, wohnort, erfahrung, social_link,
|
|
profil_sichtbarkeit, avatar_url, created_at
|
|
FROM users WHERE id=?""",
|
|
(user_id,)
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "User nicht gefunden.")
|
|
data = dict(row)
|
|
data["is_premium"] = bool(data["is_premium"])
|
|
return data
|
|
|
|
|
|
@router.patch("")
|
|
async def update_profile(data: ProfileUpdate, user=Depends(get_current_user)):
|
|
fields = data.model_dump(exclude_none=True)
|
|
|
|
# Validierungen
|
|
if "erfahrung" in fields and fields["erfahrung"] not in VALID_ERFAHRUNG:
|
|
raise HTTPException(400, f"erfahrung muss eines von {sorted(VALID_ERFAHRUNG)} sein.")
|
|
if "profil_sichtbarkeit" in fields and fields["profil_sichtbarkeit"] not in VALID_SICHTBARKEIT:
|
|
raise HTTPException(400, f"profil_sichtbarkeit muss eines von {sorted(VALID_SICHTBARKEIT)} sein.")
|
|
if "bio" in fields and len(fields["bio"]) > 300:
|
|
raise HTTPException(400, "bio darf maximal 300 Zeichen lang sein.")
|
|
if "wohnort" in fields and len(fields["wohnort"]) > 60:
|
|
raise HTTPException(400, "wohnort darf maximal 60 Zeichen lang sein.")
|
|
if "social_link" in fields and len(fields["social_link"]) > 120:
|
|
raise HTTPException(400, "social_link darf maximal 120 Zeichen lang sein.")
|
|
|
|
if not fields:
|
|
return _load_user(user["id"])
|
|
|
|
set_clause = ", ".join(f"{k}=?" for k in fields)
|
|
values = list(fields.values()) + [user["id"]]
|
|
|
|
with db() as conn:
|
|
conn.execute(
|
|
f"UPDATE users SET {set_clause} WHERE id=?", values
|
|
)
|
|
|
|
return _load_user(user["id"])
|
|
|
|
|
|
@router.post("/avatar")
|
|
async def upload_avatar(
|
|
file: UploadFile = File(...),
|
|
user=Depends(get_current_user),
|
|
):
|
|
# HEIC-Support registrieren falls vorhanden
|
|
try:
|
|
import pillow_heif
|
|
pillow_heif.register_heif_opener()
|
|
except ImportError:
|
|
pass
|
|
|
|
from PIL import Image, ImageOps
|
|
|
|
content = await file.read()
|
|
try:
|
|
img = Image.open(io.BytesIO(content))
|
|
img = ImageOps.exif_transpose(img) # EXIF-Orientierung anwenden
|
|
img = img.convert("RGB")
|
|
buf = io.BytesIO()
|
|
img.save(buf, format="JPEG", quality=90)
|
|
content = buf.getvalue()
|
|
except Exception:
|
|
pass # Fallback: Originaldaten speichern
|
|
|
|
filename = f"avatar_{user['id']}_{uuid.uuid4().hex[:8]}.jpg"
|
|
path = os.path.join(MEDIA_DIR, "avatars", filename)
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
|
|
with open(path, "wb") as f:
|
|
f.write(content)
|
|
|
|
avatar_url = f"/media/avatars/{filename}"
|
|
with db() as conn:
|
|
conn.execute(
|
|
"UPDATE users SET avatar_url=? WHERE id=?", (avatar_url, user["id"])
|
|
)
|
|
|
|
return {"avatar_url": avatar_url}
|
|
|
|
|
|
# ----------------------------------------------------------
|
|
# GET /profile/world-config — Welten-Chip-Konfiguration laden
|
|
# PUT /profile/world-config — Welten-Chip-Konfiguration speichern
|
|
# ----------------------------------------------------------
|
|
import json as _json
|
|
|
|
@router.get('/profile/world-config')
|
|
async def get_world_config(user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
row = conn.execute("SELECT world_config FROM users WHERE id=?", (user['id'],)).fetchone()
|
|
cfg = row['world_config'] if row and row['world_config'] else None
|
|
return {"config": _json.loads(cfg) if cfg else None}
|
|
|
|
|
|
class WorldConfigIn(BaseModel):
|
|
config: dict
|
|
|
|
@router.put('/profile/world-config')
|
|
async def put_world_config(body: WorldConfigIn, user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
conn.execute("UPDATE users SET world_config=? WHERE id=?",
|
|
(_json.dumps(body.config), user['id']))
|
|
return {"status": "ok"}
|