- 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
- 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
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
Audit de l'application. Cinq corrections, la première critique.
XSS stocké non authentifié (critique)
- Le journal d'activité était rendu avec `dangerouslySetInnerHTML` alors qu'il
agrège des saisies de tiers : titre de promotion d'un adhérent, et surtout nom
laissé dans le formulaire de contact PUBLIC. N'importe quel visiteur pouvait
donc faire exécuter du script dans le navigateur d'un administrateur, et
prendre la main sur son compte.
- Défense aux deux bouts : `sanitizeActivityMessage` ne laisse passer que
<strong> à l'écriture, et `activityNodes` rend le message en éléments React —
un message déjà stocké en base ne peut plus rien injecter.
SSRF
- `loadPromoImage` allait chercher toute URL http(s) placée dans le champ image
d'une promotion, et en poussait le contenu vers Facebook ou LinkedIn : de quoi
atteindre un service interne ou les métadonnées de l'hébergeur. Seules les
data-URI sont désormais acceptées, ce qui couvre tous les usages réels.
- Les images sont en plus validées à l'écriture (type et taille), côté promotion
comme côté profil adhérent.
Authentification
- Aucun frein sur la connexion : blocage progressif après 8 échecs par compte.
- La comparaison bcrypt est menée même sans compte correspondant, sinon le temps
de réponse révélait quelles adresses existent.
Aléa
- Les mots de passe temporaires venaient de `Math.random()`, prédictible :
passage à `randomInt` de node:crypto, et longueur portée à 12.
En-têtes
- nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy et
frame-ancestors. Une CSP complète demandera des nonces sur les scripts Next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
Le bandeau Mission tenait en une phrase et ne disait pas ce que l'adhésion
apporte concrètement.
- Mission réécrite en trois paragraphes : le réseau et les rencontres, la mise
en avant de chaque adhérent auprès des habitants et des professionnels, et
l'objectif double clients de proximité / courants d'affaires locaux.
- Nouveau bloc de quatre cartes sous le bandeau : se rencontrer, gagner en
visibilité (site et réseaux sociaux), attirer des clients en B to C,
développer des affaires en B to B.
- Les deux contenus sont éditables dans Paramètres : la mission accepte des
paragraphes séparés par une ligne vide, les apports suivent la convention
« Titre|Description » déjà utilisée pour le directoire.
- Migration 0009 : le texte de mission n'est remplacé que s'il est resté celui
d'origine, une version réécrite à la main n'est jamais écrasée.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
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
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
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
- Hero : logo centre verticalement (alignItems center)
- Titre "A l'honneur ce mois-ci" -> "A l'honneur"
- Remplace les 3 fiches "mise en avant" par un carrousel auto-defilant
de tous les adherents actifs, en rotation equitable (l'ordre pivote
d'un cran par heure). Pause au survol, respecte prefers-reduced-motion.
- Retire la coche "Mettre a l'honneur" du back-office (edition adherent)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Constante BASSIN_POMPEY_COMMUNES (13 communes) dans src/lib/communes.ts
- Champ commune devenu un select dans : ajout adherent, edition fiche,
espace adherent. communeOptions() conserve une valeur existante hors liste.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- getActiveMembersWithCategory renvoie coverUrl + logoUrl
- Carte annuaire affiche l'image de vitrine (coverUrl) ; si absente,
bascule sur le logo (logoUrl) ; sinon placeholder raye
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nouvelle page /backend/categories : lister, ajouter (slug + couleur auto),
renommer et supprimer des catégories. Capacité RBAC manageCategories (admin) ;
lien ajouté à la barre latérale. La suppression détache les adhérents concernés
(transaction) pour éviter toute violation de clé étrangère.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
Enrichit la fiche : couverture + avatar, identité (catégorie, localisation,
adhérent depuis, boutons Appeler/Itinéraire), à propos multi-paragraphes + tags,
informations pratiques (horaires + contact tél/email/site), carte Google Maps,
colonne promotions en cours et CTA contact.
Ajoute les champs members: cover_url, phone, website, postal_code, member_since,
hours, tags (migration 0001) ; éditables dans le back-office ; seed enrichi
(Au Bon Pain conforme à la maquette).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
Liste des adhérents actifs avec recherche (nom/métier), filtre par commune,
filtres catégories (chips + compteurs), tri (A–Z / avec promotions), état vide
et CTA d'adhésion — fidèle à la maquette. Chaque carte ouvre la fiche adhérent
(/adherents/[id]). La recherche de l'accueil et la nav pointent vers /annuaire
(la barre de recherche pré-remplit via ?q=). Nouvelles requêtes annuaire.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
Profil public d'un commerçant : vitrine, catégorie, localisation, description,
bouton de contact et promotions en cours. Réservée aux adhérents actifs (404
sinon). Les cartes « À l'honneur » de l'accueil pointent désormais vers la fiche.
Nouvelles requêtes getPublicMember / getMemberLivePromotions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL