SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
4.5 KB · 124 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# profile.py : personnalisation du profil membre5#   · PUT  /api/me/profile — nom affiché, bio, ville, téléphone, site web,6#     réseaux sociaux (instagram, facebook, x, linkedin, tiktok, youtube)7#   · POST /api/me/avatar  — téléversement de photo (Pillow : recadrage carré8#     centré + 512 px + JPEG), servie depuis /uploads/avatars/…9#   · DELETE /api/me/avatar — retour à l'avatar Google10#   Le nom affiché (display_name) prime sur le nom Google, qui est réécrasé11#   à chaque connexion ; l'avatar téléversé (avatar_url) prime sur `picture`.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import io16import json17import time18from pathlib import Path1920from fastapi import APIRouter, HTTPException, Request, UploadFile21from pydantic import BaseModel2223from . import db24from .auth import current_user2526router = APIRouter(prefix="/api")2728ROOT = Path(__file__).resolve().parent.parent29AVATAR_DIR = ROOT / "data" / "uploads" / "avatars"30AVATAR_MAX_BYTES = 8 * 1024 * 1024      # 8 Mo avant traitement31AVATAR_SIZE = 512                        # carré final (px)3233SOCIAL_KEYS = ("instagram", "facebook", "x", "linkedin", "tiktok", "youtube")343536def _require(request: Request) -> dict:37    user = current_user(request)38    if user is None:39        raise HTTPException(401, "non connecté")40    return user414243class ProfileUpdate(BaseModel):44    display_name: str = ""45    bio: str = ""46    city: str = ""47    phone: str = ""48    website: str = ""49    socials: dict[str, str] = {}505152@router.put("/me/profile")53def update_profile(request: Request, body: ProfileUpdate):54    user = _require(request)55    socials = {k: (body.socials.get(k) or "").strip()[:200]56               for k in SOCIAL_KEYS if (body.socials.get(k) or "").strip()}57    con = db.connect()58    con.execute(59        """UPDATE users SET display_name=?, bio=?, city=?, phone=?,60           website=?, socials=? WHERE id=?""",61        (body.display_name.strip()[:80], body.bio.strip()[:280],62         body.city.strip()[:60], body.phone.strip()[:40],63         body.website.strip()[:300],64         json.dumps(socials, ensure_ascii=False) if socials else "",65         user["uid"]))66    con.commit()67    con.close()68    return {"ok": True}697071@router.post("/me/avatar")72async def upload_avatar(request: Request, file: UploadFile):73    """Photo de profil : recadrée carrée, 512 px, JPEG — remplace l'ancienne."""74    user = _require(request)75    data = await file.read()76    if len(data) > AVATAR_MAX_BYTES:77        raise HTTPException(413, "image trop lourde (max 8 Mo)")78    try:79        from PIL import Image, ImageOps80        img = Image.open(io.BytesIO(data))81        img.load()82        img = ImageOps.exif_transpose(img).convert("RGB")83    except Exception:84        raise HTTPException(400, "fichier illisible — envoyez une image (JPG, PNG, WebP…)")85    # recadrage carré centré puis 512 px86    side = min(img.size)87    left = (img.width - side) // 288    top = (img.height - side) // 289    img = img.crop((left, top, left + side, top + side))90    img = img.resize((AVATAR_SIZE, AVATAR_SIZE), Image.LANCZOS)9192    con = db.connect()93    row = con.execute("SELECT ka_id, avatar_url FROM users WHERE id=?",94                      (user["uid"],)).fetchone()95    ka = (row["ka_id"] or f"uid{user['uid']}") if row else f"uid{user['uid']}"96    AVATAR_DIR.mkdir(parents=True, exist_ok=True)97    fname = f"{ka}-{int(time.time())}.jpg"98    img.save(AVATAR_DIR / fname, "JPEG", quality=88)99    # purge des anciennes photos de ce membre100    for old in AVATAR_DIR.glob(f"{ka}-*.jpg"):101        if old.name != fname:102            old.unlink(missing_ok=True)103    url = f"/uploads/avatars/{fname}"104    con.execute("UPDATE users SET avatar_url=? WHERE id=?", (url, user["uid"]))105    con.commit()106    con.close()107    return {"avatar_url": url}108109110@router.delete("/me/avatar")111def delete_avatar(request: Request):112    """Retire la photo téléversée (retour à l'avatar Google)."""113    user = _require(request)114    con = db.connect()115    row = con.execute("SELECT ka_id FROM users WHERE id=?",116                      (user["uid"],)).fetchone()117    if row and row["ka_id"]:118        for old in AVATAR_DIR.glob(f"{row['ka_id']}-*.jpg"):119            old.unlink(missing_ok=True)120    con.execute("UPDATE users SET avatar_url=NULL WHERE id=?", (user["uid"],))121    con.commit()122    con.close()123    return {"ok": True}124