TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1# Toveli — identité unique, publications directes, échanges (2026-10-17)23## État exact des accès (Phase 0)4- 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).5- Module de publication existant : aucun (les trois plateformes agrègent). Le wizard Toveli est nouveau.6- 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).7- 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).89## ADR — PostgreSQL + Drizzle à côté de la base canonique SQLite10Les 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 | <site>).1112## ADR — Resend + OTP13Code 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.1415## Rôles et capacités16Un 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`.1718## Modèle de données (PostgreSQL) — voir `src/db/schema.ts`19users, 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).2021## Publication — machine d'états22DRAFT → (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`).2324## Médias25Jeton 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).2627## Confidentialité et Loi 2528Minimisation (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.2930## Limites actuelles et prochaines étapes31- 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).32- Envoi réel de courriels : nécessite une clé Resend et un domaine vérifié (SPF/DKIM/DMARC pour `connexion@toveli.ca`).33- Stockage S3 compatible, CAPTCHA adaptatif, WebAuthn (TOTP en place), revendication d'annonces agrégées (table prête, UI à faire), applications locatives (hors v1).34