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
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
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
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
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
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
Comptes de connexion adherent
- addMember cree desormais un compte users (role member, lie a la fiche)
avec un mot de passe temporaire genere
- Le mdp temporaire s'affiche sur la fiche admin (/backend/adherents/[id])
jusqu'a la 1ere connexion ; bouton "Reinitialiser le mot de passe"
- 1ere connexion : changement de mot de passe force (must_change_password)
via /backend/changer-mot-de-passe, redirection geree dans authorized()
- users += must_change_password, temp_password (migration 0003)
Boite de reception admin (consultation)
- Page /backend/demandes : demandes d'adhesion + messages de contact
avec statuts (approuvee/rejetee, lu/archive)
- Lien sidebar + badge (nouveaux), carte tableau de bord cliquable
Espace adherent
- L'adherent peut editer sa propre fiche (updateOwnProfile) : infos,
description, horaires, tags, image d'entete + logo
- Statut/categorie/mise a l'honneur restent reserves au staff
Promotions : moderation deja en place, inchangee.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Logo en haut a gauche de la page Annuaire (prop logo sur SiteHeader)
- Modal "Devenir adherent" sur les CTA accueil + annuaire : cree une
demande d'adhesion (membership_requests) et alerte les admins via le
journal d'activite (compteur tableau de bord deja cable)
- Formulaire de contact (modal depuis le pied de page) : nouvelle table
contact_messages + alerte admin
- Composant Modal partage, server actions publiques (sans auth)
- Migration drizzle 0002
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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