Commit Graph
12 Commits
Author SHA1 Message Date
Claude bfc82b3b3b Tags adhérents automatiques : vocabulaire par métier, suggestions et repli
- src/lib/tags.ts (pur) : mots-clés par métier pour les 104 catégories,
  vocabulaire transversal détecté dans la description (fait maison, bio,
  livraison, sur rendez-vous, devis gratuit…), suggestTags() qui combine
  métier, commune et description, autoTags() qui n'intervient qu'à vide.
- Enregistrement : addMember, updateMember et updateOwnProfile passent par
  resolveMemberTags() ; un champ laissé vide reçoit les suggestions, une
  saisie n'est jamais remplacée.
- Formulaires : composant TagsField (champ non contrôlé + pastilles de
  suggestions recalculées à chaque frappe depuis catégorie, commune et
  description ; « Tout ajouter »), dans l'espace adhérent et la fiche staff.
- Fiche publique : les fiches jamais ré-enregistrées affichent les tags
  suggérés et les transmettent dans LocalBusiness.keywords.
- tests/tags.test.ts : couverture de chaque métier, nettoyage, détection,
  ordre des suggestions, non-écrasement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-06 06:51:25 +00:00
Claude c8417d8560 SEO : URLs de fiche parlantes, pages métier indexables, aides de rédaction
- Fiches adhérents : URL canonique /adherents/12-au-bon-pain-frouard
  (memberPath). L'identifiant en tête suffit ; /adherents/12 et tout slug
  périmé sont redirigés de façon permanente vers la forme canonique, donc
  les anciens liens et les partages sociaux restent valides. Tous les liens
  (accueil, carrousel, annuaire, JSON-LD, sitemap, publications Facebook /
  LinkedIn) passent par memberPath.
- Pages métier /annuaire/[categorie] : une page indexable par catégorie
  avec titre, introduction (effectif, communes), grille d'adhérents, fil
  d'Ariane et ItemList. Sans adhérent : noindex et hors sitemap. Section
  « Parcourir par métier » sur l'annuaire et « Autres métiers » sur chaque
  page pour le maillage interne.
- Composants partagés MemberCard et CategoryLinks ; slugify déplacé dans
  src/lib/slug.ts.
- Backend : conseils de rédaction sous les champs description et tags des
  formulaires adhérent (espace adhérent et fiche staff).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-06 06:29:28 +00:00
Claude f9d0d6c825 Accueil, pied de page et catégories : compteur réel, boutons alignés, référentiel complété
- Accueil : le bandeau « Faites partie de l'aventure » affiche le nombre
  d'adhérents validés (statut active) au lieu de « plus de 120 » codé en dur.
- Pied de page : les boutons Facebook / LinkedIn ont la même largeur (grille à
  une colonne dimensionnée sur le plus long) et une icône à largeur fixe.
- Catégories : référentiel unique dans src/db/categories.ts (104 métiers, dont
  assurance, bazar/discount, grande distribution, librairie-presse, tabac,
  vétérinaire, auto-école, etc.), inséré par la migration 0012 avec
  ON CONFLICT (slug) : les catégories manquantes apparaissent au prochain
  démarrage, sans écraser un libellé renommé depuis le backend. Le seed et
  l'action « Ajouter une catégorie » réutilisent ce référentiel et sa palette.
- tests/categories.test.ts vérifie unicité des slugs, contraintes de la table
  et cohérence entre le référentiel et la migration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-06 06:18:37 +00:00
Claude a8adb9ae6a Référencement : métadonnées complètes, données structurées, sitemap et robots
- Layout : metadataBase, gabarit de titre « %s · Plein R », description,
  mots-clés, Open Graph / Twitter, robots, icônes, manifeste, viewport et
  couleur de thème ; JSON-LD Organization + WebSite (recherche annuaire).
- Chaque page publique pose titre, description (≤ 160 caractères) et
  canonique via pageMetadata() ; l'annuaire ignore ainsi ?q=. Fiche adhérent :
  titre « Nom · Catégorie à Ville », description dérivée, LocalBusiness avec
  adresse, contacts et horaires (OpeningHoursSpecification), BreadcrumbList.
  Annuaire : ItemList des adhérents. Association : BusinessEvent par
  rencontre à venir. Rencontres passées : fil d'Ariane.
- Nouvelles routes : robots.txt, sitemap.xml (pages statiques + adhérents
  actifs), manifest.webmanifest, image de partage 1200×630 générée avec le
  logo, page 404 dédiée (noindex).
- /backend, /login, /inscription/* et les fiches inactives en noindex.
- Landmarks <main> sur toutes les pages publiques, fil d'Ariane en <nav>.
- serializeJsonLd() échappe <, > et & : les saisies des adhérents ne peuvent
  pas fermer le bloc <script>. Tests unitaires dans tests/seo.test.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-05 10:41:53 +00:00
Claude 741dd53b40 Base vide par défaut : données de démonstration isolées et purgeables
Le seed ne crée plus que le référentiel des catégories et le compte
administrateur initial. Les adhérents, promotions, demandes d'adhésion,
entrées du journal et comptes de démonstration sont déplacés dans
src/db/demo-data.ts et ne sont insérés qu'avec SEED_DEMO=true.

Nouveau script `npm run db:purge-demo` (bundlé en dist/purge-demo.cjs pour le
conteneur) : retire exactement ce jeu de démonstration d'une base qui l'a reçu
lors des démarrages précédents, sans toucher aux saisies de l'association ni
aux catégories. Idempotent, en transaction ; un compte réel rattaché à une
fiche de démo est détaché et non supprimé.

Vérifié en local sur Postgres 16 : seed par défaut = 59 catégories + 1 admin
et rien d'autre ; seed avec SEED_DEMO=true = jeu complet ; purge = retour à
l'état vide ; seconde purge sans effet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAQsCp4nnZbwexCg7NDBHE
2026-09-04 09:23:44 +00:00
Claude 3b17d7ff42 Durcissement (lot A) : seed sûr en production, mots de passe temporaires affichés une seule fois
Audit de sécurité complet de l'application, puis correctifs du lot A validés :

- Seed : plus aucun compte de démonstration (mot de passe « changeme123 »)
  sans SEED_DEMO=true. L'administrateur initial reçoit un mot de passe
  aléatoire affiché une fois dans les journaux (ou SEED_ADMIN_PASSWORD) et
  doit le changer à la première connexion. docker-compose ne fournit plus de
  mot de passe par défaut.
- Mots de passe temporaires : la colonne users.temp_password (en clair) est
  supprimée (migration 0011). Création d'adhérent, réinitialisation,
  rattrapage des comptes manquants, approbation de demande et invitation
  staff renvoient les identifiants, affichés une seule fois par le composant
  OneTimeCredentials, sans redirection. L'invitation staff, qui ne
  communiquait jamais le mot de passe, redevient utilisable et impose le
  changement à la première connexion.
- Sessions JWT limitées à 7 jours (30 auparavant).
- En-têtes : Strict-Transport-Security ajouté, X-Powered-By supprimé.
- docker-compose : port Postgres publié sur 127.0.0.1 uniquement.
- Image Docker sur node:22 (Node 20 en fin de vie).
- Dépendances : next 15.5.25, pg 8.23, nanoid 3.3.18 (avis GHSA-2v37-7h3g-55p8).

Vérifié en local : tests, typage, build de production, migration + seed sur
un Postgres 16, et parcours navigateur complet (première connexion, changement
forcé, création / réinitialisation / invitation avec affichage unique, cookie
de session à 7 jours).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAQsCp4nnZbwexCg7NDBHE
2026-09-04 09:15:45 +00:00
Claude b0788d3f20 Durcissement : mot de passe actuel, sessions révocables, dépendances, CSP, tests
Suite de l'audit : les cinq points laissés en suspens sont traités.

Mot de passe
- `changeOwnPassword` exige désormais le mot de passe actuel. Un poste laissé
  ouvert ne suffit plus à s'approprier un compte. Les erreurs reviennent sur
  l'écran avec un message au lieu d'une page d'erreur brute.

Sessions révocables
- Colonne `users.session_version`, portée dans le jeton et comparée à la base.
- `getSession()` (src/lib/session.ts) remplace `auth()` sur les 20 pages et
  actions : rôle, rattachement adhérent et existence du compte sont relus à
  chaque requête. Supprimer un compte ou réinitialiser un mot de passe coupe
  immédiatement les sessions ouvertes, sans attendre l'expiration du jeton.

Dépendances — de 8 vulnérabilités (2 critiques) à zéro
- next 15.5.19 → 15.5.21, next-auth beta.25 → beta.32, drizzle-orm 0.38 → 0.45.
- postcss et sharp forcés par `overrides` sur leurs versions corrigées, Next ne
  les ayant pas encore reprises ; drizzle-kit et esbuild montés côté outillage.
- `npm audit fix --force` a été écarté : il proposait de RÉTROGRADER Next en
  9.3.3 et eslint-config-next en 12, ce qui aurait cassé l'application.
- `eslint-config-next` traînait dans node_modules sans être déclaré : retiré.

CSP complète
- Politique à nonce posée par le middleware, nonce régénéré à chaque requête,
  `script-src` sans 'unsafe-inline'. `style-src` garde 'unsafe-inline' : tout le
  design repose sur des attributs style, et une injection de style n'a pas la
  portée d'une injection de script.
- Conséquence assumée : rendu dynamique pour toutes les pages, un HTML
  pré-généré ne pouvant pas porter de nonce.

Tests
- `npm test` (runner natif node:test via tsx), 18 tests sur le filtre XSS, la
  limitation des tentatives de connexion et le chiffrement des jetons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-25 10:18:04 +00:00
Claude e86c67c17e URL publique du site réglable depuis le backoffice
Connecter un réseau social imposait de poser NEXT_PUBLIC_SITE_URL dans
docker-compose et de redémarrer le conteneur. L'écran Réseaux sociaux porte
désormais le champ, pré-rempli avec l'adresse par laquelle l'administrateur
consulte la page.

- Nouveau réglage `site_public_url` ; `siteUrl()` lit la base puis retombe sur
  les variables d'environnement, donc les déploiements actuels ne bougent pas.
- `siteUrl()`, `redirectUri()`, `authorizeUrl()`, `promoLink()` et
  `buildPromoMessage()` deviennent asynchrones.
- Seule l'origine est conservée : le schéma est ajouté s'il manque, un chemin
  est retiré, et une adresse invalide revient sur l'écran avec un message au
  lieu d'une page d'erreur brute.
- `saveSiteSettings` saute cette clé : le formulaire Paramètres ne la contient
  pas et l'aurait écrasée à chaque enregistrement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-25 08:28:04 +00:00
Claude 19d1104ab0 Connexion Facebook / LinkedIn en OAuth depuis le backoffice
Les jetons se posaient à la main dans docker-compose. Un écran Backend ›
Réseaux sociaux permet désormais de coller les identifiants de l'application,
cliquer Connecter, choisir la page, et ne plus y revenir.

- Table `social_accounts` : identifiants, jetons et cible par réseau. Tous les
  secrets sont chiffrés en AES-256-GCM (`src/lib/crypto.ts`), clé
  `SOCIAL_TOKEN_KEY` avec repli sur `AUTH_SECRET`.
- Routes `api/social/[network]/{connect,callback}` : `state` anti-CSRF en cookie
  httpOnly, échange du code, récupération des pages administrées. Aucun jeton ne
  transite par une URL — les cibles sont relistées côté serveur au moment de la
  sélection.
- Facebook : jeton utilisateur longue durée puis jeton de PAGE, qui n'expire
  pas. LinkedIn : jeton 60 jours, rafraîchi automatiquement si l'application a
  obtenu les jetons de rafraîchissement programmatiques.
- Faute de quoi le backoffice affiche la date d'expiration, un bandeau sur le
  tableau de bord à J-7 et un bouton Reconnecter.
- `social.ts` lit les identifiants via `social-accounts.ts` : base d'abord,
  variables d'environnement ensuite. Les installations existantes continuent de
  fonctionner sans modification.
- Le secret d'application n'est jamais renvoyé au navigateur : champ vide =
  valeur conservée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-24 23:00:08 +00:00
Claude 8bc1b17fc5 Diffusion réseaux pilotée par la validation de la promotion
Le bouton libre « Publier sur X » permettait de diffuser n'importe quelle promo
en ligne, à tout moment. Le choix appartient désormais à l'adhérent, la
publication part à la validation, et le choix est figé ensuite.

- Colonnes `promotions.share_facebook` / `share_linkedin` : réseaux demandés à la
  soumission, modifiables tant que la promo est `pending`.
- L'adhérent coche les réseaux dans le formulaire de soumission et peut encore
  les changer depuis Mes promotions tant qu'elle est en attente ; une fois
  validée, il ne voit plus qu'un état en lecture seule.
- Le modérateur retrouve ces cases pré-cochées dans le formulaire « Valider » et
  peut les ajuster : c'est le dernier moment où la diffusion est modifiable.
  Valider met en ligne puis publie, dans cet ordre, afin qu'un réseau en panne
  ne bloque pas la mise en ligne.
- `publishPromoShares` devient le seul point de publication. Elle ne lève jamais
  et ignore tout réseau ayant déjà une publication réussie : une promo n'est
  jamais postée deux fois, y compris après un cycle suspension → remise en ligne.
- `sharePromoToSocial` est remplacée par `retryPromoShare`, limitée au rattrapage
  d'un échec sur un réseau déjà choisi, sur une promo en ligne uniquement.
- Un réseau non configuré sur le serveur enregistre un échec explicite plutôt
  que d'échouer en silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-24 22:41:25 +00:00
Claude e3a4771edf Refonte du bloc infos pratiques, suspension des promos et publication réseaux
Bloc « Informations pratiques » (fiche adhérent)
- Suppression des dégradés hors palette (radial doré de la section, dégradés
  crème/bleu des deux sous-cartes) au profit du vocabulaire visuel du site :
  carte blanche bordée, sous-cartes plates sur le fond de champ, variables CSS.
- La pastille « Préparer votre visite » devient une puce arrondie identique aux
  badges du reste du site ; le chip d'ouverture reprend les teintes de statut.
- Les glyphes texte ◷ et ↗ sont remplacés par des icônes SVG cohérentes.

Suspension des promotions
- Nouveau statut `suspended` + colonnes `suspended_by` / `suspended_by_id` /
  `suspended_at`.
- L'adhérent suspend et réactive ses propres promotions depuis Mon espace ;
  l'association suspend et réactive n'importe laquelle depuis le backoffice.
- Une suspension décidée par l'association ne peut pas être levée par
  l'adhérent : le motif est affiché dans son espace.
- Les lectures publiques filtrant déjà sur `live`, une promo suspendue quitte
  immédiatement l'accueil, l'annuaire et la fiche.

Publication Facebook / LinkedIn
- `src/lib/social.ts` : publication d'une promo en ligne sur la page Facebook
  (Graph API, upload multipart car les images sont en data-URI) et sur la page
  LinkedIn (Posts API + Images API en trois étapes).
- Jetons d'accès exclusivement en variables d'environnement ; un réseau non
  configuré masque son bouton.
- Table `social_posts` : chaque tentative est tracée, un échec est rattrapé et
  affiché sur la carte de la promo sans interrompre le backoffice.
- Nouvelle capacité RBAC `publishSocial` (admin + modérateur).

Liens sociaux publics
- Réglages `association_facebook` / `association_linkedin` éditables dans
  Paramètres, préremplis avec les pages de l'association.
- Section « Plein R sur les réseaux » sur l'accueil et icônes dans le pied de
  page, masquées si le réglage est vide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-24 22:20:51 +00:00
Claude 7e22ec50d4 Implémente le site Plein R (Accueil + Backend) sur Next.js + Postgres + Docker
Application unique Next.js (App Router, TypeScript) servant :
- Accueil public : héro, recherche, métiers, promotions, mises à l'honneur
  (données issues de Postgres)
- Backend authentifié avec RBAC : tableau de bord, adhérents (CRUD), modération
  des promotions, administrateurs, espace adhérent (publication avec aperçu)

Technique :
- Drizzle ORM + drizzle-kit (migrations versionnées) sur driver pg
- Auth.js v5 (credentials, sessions JWT), rôles admin/moderator/editor/member
- Middleware de protection de /backend + gardes par rôle dans chaque vue
- Server actions pour toutes les mutations (vérif auth + capacité + revalidate)
- Design fidèle aux maquettes (design-system CSS, animations, responsive)

Conteneurisation :
- 1 conteneur applicatif (sortie standalone) + 1 conteneur Postgres externe
- docker-compose avec healthcheck ; migrations + seed au démarrage
- scripts migrate/seed bundlés (esbuild) pour un runtime sans devDeps

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 12:06:21 +00:00