SPB Git forge

spb/rent-ka

Public
8commits 1branches 0releases
7.4 MBsize
maindefault branch
20 days agolast push
Python 68.8% TypeScript 18.6% CSS 8.7% JavaScript 3.3% HTML 0.6%
9.1 KB · 248 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Rent-Ka — Rental listings aggregator (Canada, outside Québec)3# Author: Simon-Pierre Boucher — contact@spboucher.ai4# accounts.py : fonctionnalités de compte au-delà de l'authentification5#   · Rôle : « locataire » (cherche un logement) ou « gestionnaire » (gère des6#     logements affichés) — choisi à l'accueil après la création du compte.7#   · Locataire : favoris (POST/DELETE /api/favorites/{uid}, GET /api/favorites).8#   · Gestionnaire : réclame la page de SA source (première arrivée, une source9#     par compte, un compte par source) puis la personnalise (accroche,10#     description, coordonnées, logo) — affichée publiquement sur /g/{source}.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import json15import time16from pathlib import Path1718from fastapi import APIRouter, HTTPException, Request19from pydantic import BaseModel2021from . import db, hubfav, kaid22from .auth import current_user2324SOURCES_PATH = Path(__file__).resolve().parent.parent / "data" / "sources.json"2526router = APIRouter(prefix="/api")2728ROLES = ("locataire", "gestionnaire")293031def _require(request: Request) -> dict:32    user = current_user(request)33    if user is None:34        raise HTTPException(401, "non connecté")35    return user363738def _row_listing(row) -> dict:39    d = dict(row)40    d["amenities"] = json.loads(d.get("amenities") or "[]")41    d["images"] = json.loads(d.get("images") or "[]")42    d["details"] = json.loads(d.get("details") or "{}")43    if d.get("furnished") is not None:44        d["furnished"] = bool(d["furnished"])45    return d464748# -- rôle -----------------------------------------------------------------------4950@router.post("/me/role")51def set_role(request: Request, role: str):52    user = _require(request)53    if role not in ROLES:54        raise HTTPException(400, f"rôle invalide (choix : {', '.join(ROLES)})")55    con = db.connect()56    con.execute("UPDATE users SET role=? WHERE id=?", (role, user["uid"]))57    con.commit()58    con.close()59    return {"role": role}606162# -- favoris (locataire) ---------------------------------------------------------6364@router.get("/favorites")65def favorites(request: Request):66    user = _require(request)67    con = db.connect()68    rows = con.execute(69        """SELECT l.* FROM favorites f JOIN listings l ON l.uid = f.uid70           WHERE f.user_id=? ORDER BY f.ts DESC""", (user["uid"],)).fetchall()71    uids = [r["uid"] for r in con.execute(72        "SELECT uid FROM favorites WHERE user_id=?", (user["uid"],))]73    con.close()74    return {"uids": uids, "listings": [_row_listing(r) for r in rows]}757677def _ka_id_of(con, user_id: int) -> str | None:78    row = con.execute("SELECT ka_id FROM users WHERE id=?", (user_id,)).fetchone()79    return row["ka_id"] if row else None808182@router.post("/favorites/{uid}")83def add_favorite(request: Request, uid: str):84    user = _require(request)85    con = db.connect()86    listing = con.execute("SELECT * FROM listings WHERE uid=?", (uid,)).fetchone()87    if listing is None:88        con.close()89        raise HTTPException(404, "annonce introuvable")90    con.execute("INSERT OR IGNORE INTO favorites (user_id, uid, ts) VALUES (?,?,?)",91                (user["uid"], uid, time.time()))92    con.commit()93    ka_id = _ka_id_of(con, user["uid"])94    con.close()95    # miroir « Mon univers Ka » sur le hub (arrière-plan, jamais bloquant)96    hubfav.push_favorite(ka_id, "add", hubfav.listing_item(listing))97    d = dict(listing)98    kaid.track({"ka_id": ka_id}, "favorite", entity_type="listing",99               entity_id=uid, features={k: v for k, v in {100                   "city": d.get("city"), "sector": d.get("sector"),101                   "unit_type": d.get("unit_type"), "price": d.get("price"),102                   "pets": d.get("pets")}.items() if v not in (None, "")})103    return {"ok": True}104105106@router.delete("/favorites/{uid}")107def remove_favorite(request: Request, uid: str):108    user = _require(request)109    con = db.connect()110    con.execute("DELETE FROM favorites WHERE user_id=? AND uid=?",111                (user["uid"], uid))112    con.commit()113    ka_id = _ka_id_of(con, user["uid"])114    con.close()115    hubfav.push_favorite(ka_id, "remove", {"item_id": uid})116    kaid.track({"ka_id": ka_id}, "unfavorite", entity_type="listing",117               entity_id=uid)118    return {"ok": True}119120121# -- page gestionnaire -----------------------------------------------------------122123def _source_name(source_id: str) -> str | None:124    try:125        registry = json.loads(SOURCES_PATH.read_text(encoding="utf-8"))["sources"]126    except Exception:127        return None128    for s in registry:129        if s["id"] == source_id:130            return s.get("name") or source_id131    return None132133134class OrgUpdate(BaseModel):135    tagline: str = ""136    description: str = ""137    website: str = ""138    phone: str = ""139    email: str = ""140    logo_url: str = ""141142143@router.post("/org/claim")144def claim_org(request: Request, source: str):145    """Réclame la page d'une source (gestionnaire) — 1 source/compte, 1 compte/source."""146    user = _require(request)147    name = _source_name(source)148    if name is None:149        raise HTTPException(404, "source inconnue")150    con = db.connect()151    me_row = con.execute("SELECT role, org_source FROM users WHERE id=?",152                         (user["uid"],)).fetchone()153    if me_row["role"] != "gestionnaire":154        con.close()155        raise HTTPException(403, "réservé aux comptes gestionnaires")156    if me_row["org_source"] and me_row["org_source"] != source:157        con.close()158        raise HTTPException(409, "votre compte gère déjà une autre page")159    owner = con.execute(160        "SELECT owner_user_id FROM source_profiles WHERE source_id=?",161        (source,)).fetchone()162    if owner and owner["owner_user_id"] not in (None, user["uid"]):163        con.close()164        raise HTTPException(409, "cette page est déjà réclamée par un autre compte")165    con.execute(166        """INSERT INTO source_profiles (source_id, owner_user_id, updated_at)167           VALUES (?,?,?)168           ON CONFLICT(source_id) DO UPDATE SET169             owner_user_id=excluded.owner_user_id, updated_at=excluded.updated_at""",170        (source, user["uid"], time.time()))171    con.execute("UPDATE users SET org_source=? WHERE id=?", (source, user["uid"]))172    con.commit()173    con.close()174    return {"source": source, "name": name}175176177@router.get("/org/mine")178def my_org(request: Request):179    user = _require(request)180    con = db.connect()181    me_row = con.execute("SELECT org_source FROM users WHERE id=?",182                         (user["uid"],)).fetchone()183    source = me_row["org_source"] if me_row else None184    if not source:185        con.close()186        return {"source": None}187    prof = con.execute("SELECT * FROM source_profiles WHERE source_id=?",188                       (source,)).fetchone()189    n = con.execute(190        "SELECT COUNT(*) c FROM listings WHERE source=? AND active=1",191        (source,)).fetchone()["c"]192    con.close()193    return {194        "source": source,195        "name": _source_name(source) or source,196        "active_listings": n,197        "profile": dict(prof) if prof else {},198    }199200201@router.put("/org")202def update_org(request: Request, body: OrgUpdate):203    user = _require(request)204    con = db.connect()205    me_row = con.execute("SELECT org_source FROM users WHERE id=?",206                         (user["uid"],)).fetchone()207    source = me_row["org_source"] if me_row else None208    if not source:209        con.close()210        raise HTTPException(409, "aucune page réclamée")211    owner = con.execute(212        "SELECT owner_user_id FROM source_profiles WHERE source_id=?",213        (source,)).fetchone()214    if not owner or owner["owner_user_id"] != user["uid"]:215        con.close()216        raise HTTPException(403, "vous n'êtes pas propriétaire de cette page")217    con.execute(218        """UPDATE source_profiles SET tagline=?, description=?, website=?,219           phone=?, email=?, logo_url=?, updated_at=? WHERE source_id=?""",220        (body.tagline.strip()[:140], body.description.strip()[:2000],221         body.website.strip()[:300], body.phone.strip()[:40],222         body.email.strip()[:120], body.logo_url.strip()[:500],223         time.time(), source))224    con.commit()225    con.close()226    return {"ok": True}227228229@router.get("/org/{source_id}")230def public_org(source_id: str):231    """Page publique d'un gestionnaire : nom + personnalisation + inventaire."""232    name = _source_name(source_id)233    if name is None:234        raise HTTPException(404, "source inconnue")235    con = db.connect()236    prof = con.execute("SELECT * FROM source_profiles WHERE source_id=?",237                       (source_id,)).fetchone()238    n = con.execute(239        "SELECT COUNT(*) c FROM listings WHERE source=? AND active=1",240        (source_id,)).fetchone()["c"]241    con.close()242    out = {"source": source_id, "name": name, "active_listings": n,243           "claimed": bool(prof and prof["owner_user_id"])}244    if prof:245        for f in ("tagline", "description", "website", "phone", "email", "logo_url"):246            out[f] = prof[f] or ""247    return out248