Sprint 14: User-Profil-System (bio, wohnort, erfahrung, avatar)
- DB-Migrationen: 7 neue Spalten in users (bio, wohnort, erfahrung, social_link, profil_sichtbarkeit, avatar_url, email_verified) - GET /api/auth/me gibt nun alle Profil-Felder + created_at zurück - PATCH /api/profile: Profil-Felder aktualisieren (mit Validierung) - POST /api/profile/avatar: Avatar-Upload (PIL JPEG-Konvertierung, HEIC-Support) - Router in main.py registriert
This commit is contained in:
parent
41c4ba3dd6
commit
9bd8701a1d
4 changed files with 134 additions and 7 deletions
|
|
@ -444,6 +444,14 @@ def _migrate(conn_factory):
|
||||||
("users", "ban_reason", "TEXT"),
|
("users", "ban_reason", "TEXT"),
|
||||||
# WebCal: Kalender-Abo-Token
|
# WebCal: Kalender-Abo-Token
|
||||||
("users", "calendar_token", "TEXT"),
|
("users", "calendar_token", "TEXT"),
|
||||||
|
# User-Profil-Felder
|
||||||
|
("users", "email_verified", "INTEGER NOT NULL DEFAULT 0"),
|
||||||
|
("users", "bio", "TEXT"),
|
||||||
|
("users", "wohnort", "TEXT"),
|
||||||
|
("users", "erfahrung", "TEXT"),
|
||||||
|
("users", "social_link", "TEXT"),
|
||||||
|
("users", "profil_sichtbarkeit", "TEXT NOT NULL DEFAULT 'public'"),
|
||||||
|
("users", "avatar_url", "TEXT"),
|
||||||
]
|
]
|
||||||
with conn_factory() as conn:
|
with conn_factory() as conn:
|
||||||
for table, column, col_type in migrations:
|
for table, column, col_type in migrations:
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ from routes.friends import router as friends_router
|
||||||
from routes.chat import router as chat_router
|
from routes.chat import router as chat_router
|
||||||
from routes.admin import router as admin_router
|
from routes.admin import router as admin_router
|
||||||
from routes.webcal import router as webcal_router
|
from routes.webcal import router as webcal_router
|
||||||
|
from routes.profile import router as profile_router
|
||||||
|
|
||||||
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
|
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
|
||||||
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
|
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
|
||||||
|
|
@ -96,6 +97,7 @@ app.include_router(friends_router, prefix="/api/friends", tags=["Freunde"]
|
||||||
app.include_router(chat_router, prefix="/api/chat", tags=["Chat"])
|
app.include_router(chat_router, prefix="/api/chat", tags=["Chat"])
|
||||||
app.include_router(admin_router, prefix="/api/admin", tags=["Admin"])
|
app.include_router(admin_router, prefix="/api/admin", tags=["Admin"])
|
||||||
app.include_router(webcal_router, prefix="/api/webcal", tags=["WebCal"])
|
app.include_router(webcal_router, prefix="/api/webcal", tags=["WebCal"])
|
||||||
|
app.include_router(profile_router, prefix="/api/profile", tags=["Profil"])
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -92,10 +92,16 @@ async def logout(response: Response):
|
||||||
|
|
||||||
@router.get("/me")
|
@router.get("/me")
|
||||||
async def me(user=Depends(get_current_user)):
|
async def me(user=Depends(get_current_user)):
|
||||||
return {
|
with db() as conn:
|
||||||
"id": user["id"],
|
row = conn.execute(
|
||||||
"name": user["name"],
|
"""SELECT id, name, email, rolle, is_premium, email_verified,
|
||||||
"email": user["email"],
|
bio, wohnort, erfahrung, social_link,
|
||||||
"rolle": user["rolle"],
|
profil_sichtbarkeit, avatar_url, created_at
|
||||||
"is_premium": bool(user["is_premium"]),
|
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
|
||||||
|
|
|
||||||
111
backend/routes/profile.py
Normal file
111
backend/routes/profile.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
"""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):
|
||||||
|
bio: Optional[str] = None
|
||||||
|
wohnort: Optional[str] = None
|
||||||
|
erfahrung: Optional[str] = None
|
||||||
|
social_link: Optional[str] = None
|
||||||
|
profil_sichtbarkeit: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_user(user_id: int) -> dict:
|
||||||
|
with db() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"""SELECT id, 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
|
||||||
|
|
||||||
|
content = await file.read()
|
||||||
|
try:
|
||||||
|
img = Image.open(io.BytesIO(content)).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}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue