SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
2 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
6.0 KB

# Toveli — identité unique, publications directes, échanges (2026-10-17)

# État exact des accès (Phase 0)

  • Dépôts sources audités en lecture seule (voir docs/source-audit/). Aucun module d'authentification réutilisable : Lou-Ka/Immo-Ka/Vrai-Prix utilisent Google OAuth + un SSO JWT partagé (hub externe) et des favoris stockés sur ce hub → exclus (identité Toveli indépendante, OTP courriel).
  • Module de publication existant : aucun (les trois plateformes agrègent). Le wizard Toveli est nouveau.
  • Resend : aucune clé n'est présente dans le code ni dans les manifestes ; RESEND_API_KEY vide ⇒ transport simulé (data/outbox/mails.jsonl). Toute clé déjà divulguée doit être révoquée et remplacée dans le gestionnaire de secrets (manifeste mld, mode 600, hors git).
  • PostgreSQL 17 installé sur M3U96b (Homebrew), écoute 127.0.0.1/::1 uniquement, scram-sha-256 pour TCP, rôle toveli_app propriétaire de la base toveli (pas superuser). Port 5432 jamais exposé (pare-feu + bind local).

# ADR — PostgreSQL + Drizzle à côté de la base canonique SQLite

Les annonces agrégées (117 k) sont reconstruites chaque nuit dans data/toveli.db (lecture seule). Les données Toveli (comptes, annonces directes, médias, messages) exigent transactions, verrous (OTP, visites), contraintes et sauvegardes → PostgreSQL local, Drizzle ORM (migrations SQL versionnées dans drizzle/, appliquées par pnpm db:migrate à chaque déploiement, journal __drizzle_migrations). La recherche unifiée (src/lib/search.ts) fusionne les deux mondes en mémoire (volume direct faible) avec un seul contrat ListingCard (sourceId toveli | immo-ka | lou-ka, connector proprietaire | professionnel | ).

# ADR — Resend + OTP

Code 6 chiffres CSPRNG (randomInt), 10 min, usage unique, 5 essais, HMAC-SHA256(code, OTP_HMAC_SECRET, id du défi) stocké, jamais le code ; vérification dans une transaction SELECT … FOR UPDATE (double soumission → une seule réussite, testé) ; réponse neutre à la demande ; fenêtres glissantes par IP/adresse (10/10 min, 5/h, 12/j, 60/j IP), délai de renvoi 60 s ; aucun blocage irréversible. Un code correct sans acceptation des conditions (nouveau compte) renvoie 409 sans consommer le code. Courriel « Votre code de connexion Toveli » (HTML responsive + texte). Sessions : jeton 32 octets aléatoires, HMAC stocké, cookie tv_session HttpOnly/Secure/SameSite=Lax, 30 j, inactivité 14 j, révocation unitaire ou globale ; CSRF = SameSite + vérification Origin/Referer sur toute mutation. Changement d'adresse : code envoyé à la nouvelle adresse, autres sessions révoquées. Champs sensibles (téléphone, secret TOTP) chiffrés AES-256-GCM.

# Rôles et capacités

Un compte = toutes les capacités (chercher, favoris, alertes, publier, échanger). OWNER n'est pas un rôle : c'est listings.owner_user_id. user_roles (ADMIN, MODERATOR, AGENT_VERIFIED) attribués côté serveur (scripts/grant-role.ts ou console admin) avec journal. Admin : rôle + TOTP (otpauth) vérifié < 30 min, toutes les mutations journalisées (audit_events, append-only), moderation_actions.

# Modèle de données (PostgreSQL) — voir src/db/schema.ts

users, user_profiles, otp_challenges, sessions, user_roles, properties, listings (source_type DIRECT_OWNER|DIRECT_AGENT|AGGREGATED_PARTNER, status machine d'états, version optimiste, CHECK prix ≥ 0, owner obligatoire pour DIRECT), listing_media, listing_versions, listing_claims (revendication d'une annonce agrégée : jamais de transfert automatique), favorites, saved_searches (+alertes), email_deliveries, inquiries, messages (idempotence), viewing_requests (REQUESTED|CONFIRMED|CANCELLED|DONE, conflit < 30 min), reports, moderation_actions, audit_events, rate_limits. rental_applications : non créée volontairement (v1 n'exige aucun document pour contacter ; à concevoir avec rétention stricte).

# Publication — machine d'états

DRAFT → (publish) PUBLISHED | PENDING_REVIEW (drapeaux spam : liens multiples, formulations d'arnaque, loyer < 350 $, majuscules) → admin approve/reject. PUBLISHED ⇄ PAUSED ; → SOLD | RENTED | ARCHIVED → reopen → DRAFT. Pré-requis : municipalité, position, prix plausible, titre ≥ 8, description ≥ 40, ≥ 1 photo, attestation du droit de publier (horodatée), chambres sauf terrain/commercial. Expiration 90 j (rappel J-7, mise en pause automatique par scripts/jobs.ts).

# Médias

Jeton d'envoi à usage unique (équivalent presign local) → PUT /api/uploads/<token> → sniff des magic bytes (JPEG/PNG/WebP/HEIC), HEIC converti (heic-convert) ou rejeté proprement, ré-encodage WebP 1600/800/320 via sharp (neutralise les charges utiles, retire EXIF/GPS/ICC), min 200 px, max 25 Mo / 30 photos. Stockage disque STORAGE_DIR servi par /api/media/<ym>/<hex>/<w>.webp (clé validée). Pas d'antivirus dédié : limite documentée (ré-encodage + absence d'exécution).

# Confidentialité et Loi 25

Minimisation (courriel seul pour s'inscrire), coordonnées jamais publiques, masquage automatique des courriels/téléphones/liens dans les messages, adresse exacte masquable (rue ou secteur, position brouillée), export JSON et suppression de compte en libre-service (annonces archivées, messages anonymisés, compte anonymisé), journaux minimisés (IP hachées) 12 mois, politique de confidentialité et conditions provisoires (/confidentialite, /conditions) à valider juridiquement avant ouverture publique des comptes.

# Limites actuelles et prochaines étapes

  • Playwright : captures QA (scripts/screenshots.mjs) ; scénarios E2E complets à écrire (le test API bout en bout scripts/smoke.sh couvre connexion → publication → contact → visite).
  • Envoi réel de courriels : nécessite une clé Resend et un domaine vérifié (SPF/DKIM/DMARC pour connexion@toveli.ca).
  • Stockage S3 compatible, CAPTCHA adaptatif, WebAuthn (TOTP en place), revendication d'annonces agrégées (table prête, UI à faire), applications locatives (hors v1).