# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # profile.py : personnalisation du profil membre # · PUT /api/me/profile — nom affiché, bio, ville, téléphone, site web, # réseaux sociaux (instagram, facebook, x, linkedin, tiktok, youtube) # · POST /api/me/avatar — téléversement de photo (Pillow : recadrage carré # centré + 512 px + JPEG), servie depuis /uploads/avatars/… # · DELETE /api/me/avatar — retour à l'avatar Google # Le nom affiché (display_name) prime sur le nom Google, qui est réécrasé # à chaque connexion ; l'avatar téléversé (avatar_url) prime sur `picture`. # ----------------------------------------------------------------------------- from __future__ import annotations import io import json import time from pathlib import Path from fastapi import APIRouter, HTTPException, Request, UploadFile from pydantic import BaseModel from . import db from .auth import current_user router = APIRouter(prefix="/api") ROOT = Path(__file__).resolve().parent.parent AVATAR_DIR = ROOT / "data" / "uploads" / "avatars" AVATAR_MAX_BYTES = 8 * 1024 * 1024 # 8 Mo avant traitement AVATAR_SIZE = 512 # carré final (px) SOCIAL_KEYS = ("instagram", "facebook", "x", "linkedin", "tiktok", "youtube") def _require(request: Request) -> dict: user = current_user(request) if user is None: raise HTTPException(401, "non connecté") return user class ProfileUpdate(BaseModel): display_name: str = "" bio: str = "" city: str = "" phone: str = "" website: str = "" socials: dict[str, str] = {} @router.put("/me/profile") def update_profile(request: Request, body: ProfileUpdate): user = _require(request) socials = {k: (body.socials.get(k) or "").strip()[:200] for k in SOCIAL_KEYS if (body.socials.get(k) or "").strip()} con = db.connect() con.execute( """UPDATE users SET display_name=?, bio=?, city=?, phone=?, website=?, socials=? WHERE id=?""", (body.display_name.strip()[:80], body.bio.strip()[:280], body.city.strip()[:60], body.phone.strip()[:40], body.website.strip()[:300], json.dumps(socials, ensure_ascii=False) if socials else "", user["uid"])) con.commit() con.close() return {"ok": True} @router.post("/me/avatar") async def upload_avatar(request: Request, file: UploadFile): """Photo de profil : recadrée carrée, 512 px, JPEG — remplace l'ancienne.""" user = _require(request) data = await file.read() if len(data) > AVATAR_MAX_BYTES: raise HTTPException(413, "image trop lourde (max 8 Mo)") try: from PIL import Image, ImageOps img = Image.open(io.BytesIO(data)) img.load() img = ImageOps.exif_transpose(img).convert("RGB") except Exception: raise HTTPException(400, "fichier illisible — envoyez une image (JPG, PNG, WebP…)") # recadrage carré centré puis 512 px side = min(img.size) left = (img.width - side) // 2 top = (img.height - side) // 2 img = img.crop((left, top, left + side, top + side)) img = img.resize((AVATAR_SIZE, AVATAR_SIZE), Image.LANCZOS) con = db.connect() row = con.execute("SELECT ka_id, avatar_url FROM users WHERE id=?", (user["uid"],)).fetchone() ka = (row["ka_id"] or f"uid{user['uid']}") if row else f"uid{user['uid']}" AVATAR_DIR.mkdir(parents=True, exist_ok=True) fname = f"{ka}-{int(time.time())}.jpg" img.save(AVATAR_DIR / fname, "JPEG", quality=88) # purge des anciennes photos de ce membre for old in AVATAR_DIR.glob(f"{ka}-*.jpg"): if old.name != fname: old.unlink(missing_ok=True) url = f"/uploads/avatars/{fname}" con.execute("UPDATE users SET avatar_url=? WHERE id=?", (url, user["uid"])) con.commit() con.close() return {"avatar_url": url} @router.delete("/me/avatar") def delete_avatar(request: Request): """Retire la photo téléversée (retour à l'avatar Google).""" user = _require(request) con = db.connect() row = con.execute("SELECT ka_id FROM users WHERE id=?", (user["uid"],)).fetchone() if row and row["ka_id"]: for old in AVATAR_DIR.glob(f"{row['ka_id']}-*.jpg"): old.unlink(missing_ok=True) con.execute("UPDATE users SET avatar_url=NULL WHERE id=?", (user["uid"],)) con.commit() con.close() return {"ok": True}