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
5.7 KiB
CLAUDE.md
Guidance for working in this repository.
What this is
Plein R — website for an association of merchants/businesses in the Bassin de Pompey. A single Next.js application serving a public Accueil page and an authenticated, role-based Backend (admin + member space). Postgres is an external container.
Stack
- Next.js 15 (App Router, TypeScript,
output: "standalone") - PostgreSQL 16 + Drizzle ORM (
src/db/schema.ts) withdrizzle-kitmigrations - Auth.js v5 (credentials provider, JWT sessions) — RBAC in
src/lib/rbac.ts - One app container + one Postgres container (
docker-compose.yml)
Commands
npm run dev # dev server
npm run build # production build
npm run db:generate # regenerate SQL after editing src/db/schema.ts
npm run db:migrate # apply migrations
npm run db:seed # seed demo data
docker compose up --build # full stack
Conventions
- Styling: faithful port of the original design as inline styles + a small
design-system in
src/app/globals.css(palette as CSS vars, fonts, twinkle/float animations, hover lifts, responsive grid helpers). No Tailwind. - Mutations: server actions in
src/app/backend/actions.ts. Each action re-checks auth + capability viagetSession()andcan()before writing, thenrevalidatePath(). - Access control:
src/middleware.tsgates/backend/*; each page further guards by role (isStaff,can) and redirects. - Data reads for the public site live in
src/lib/queries.ts. - After changing
src/db/schema.ts, runnpm run db:generateand commit the new file underdrizzle/.
Promotions
Statuts : pending → live → suspended ⇄ live (+ rejected / expired).
promotions.suspended_by retient qui a suspendu (member ou staff) : une
suspension par le staff ne peut être levée que par le staff. Les lectures
publiques filtrent sur status = 'live', donc une promo suspendue disparaît du
site sans traitement supplémentaire.
Réseaux sociaux
src/lib/social.tspublie une promo sur la page Facebook (Graph API) ou LinkedIn (Posts API).src/lib/social-accounts.tsgère la configuration : OAuth, jetons, cibles. Réseau non configuré = case masquée.- Les identifiants et jetons vivent en base (
social_accounts), chiffrés viasrc/lib/crypto.ts(AES-256-GCM, cléSOCIAL_TOKEN_KEYouAUTH_SECRET), posés depuis/backend/reseaux. Les variables d'environnement restent lues en repli. Aucun secret ne doit jamais repartir vers le navigateur. isNetworkConfigured()/configuredNetworks()/siteUrl()/redirectUri()sont asynchrones (accès base).- L'URL publique est le réglage
site_public_url, édité sur/backend/reseaux(pré-rempli depuis les en-têtes de la requête).saveSiteSettingssaute cette clé : le formulaire Paramètres ne la contient pas et l'écraserait. - Routes OAuth :
src/app/api/social/[network]/{connect,callback}. Lestateanti-CSRF passe par un cookie httpOnly ; aucun jeton ne transite par une URL. - Facebook : le jeton de page n'expire pas. LinkedIn : 60 jours, rafraîchissement programmatique réservé à certains partenaires, d'où le bandeau de reconnexion.
- Les images de promo sont stockées en data-URI : l'upload se fait donc en binaire (multipart pour Facebook, Images API en 3 étapes pour LinkedIn), pas par URL.
- La diffusion est déclenchée par la validation, pas par un bouton :
promotions.share_facebook/share_linkedinsont choisis par l'adhérent, ajustables par le modérateur dans le formulaire « Valider », puis figés (status !== 'pending'). publishPromoShares()dansbackend/actions.tsest le seul point de publication. Elle ne lève jamais et ignore tout réseau ayant déjà une lignesocial_postsenposted: c'est la garde anti-republication, qui couvre aussi le cycle suspension → remise en ligne.retryPromoSharene sert qu'au rattrapage d'un échec sur un réseau déjà choisi ; il ne peut pas élargir la diffusion.- Les URLs publiques des pages FB/LinkedIn sont des
site_settings(association_facebook,association_linkedin), éditables dans Paramètres.
Sécurité
- Le journal d'activité agrège des saisies de tiers, dont le formulaire de
contact public : il est filtré à l'écriture (
sanitizeActivityMessage) et rendu en éléments React (activityNodes), jamais en HTML brut. getSession()(src/lib/session.ts) remplaceauth()partout : le rôle et le rattachement adhérent sont relus en base à chaque requête, etusers.session_versioninvalide les jetons émis avant un changement de mot de passe. N'appelez plusauth()directement depuis une page ou une action.- Les images ne sont acceptées qu'en data-URI (
asImageDataUri) : une URL ferait appeler par le serveur une cible choisie par l'utilisateur (SSRF). - La CSP à nonce est posée par
src/middleware.ts. Elle impose un rendu dynamique :export const dynamic = "force-dynamic"est dansapp/layout.tsx, un HTML pré-généré ne pouvant pas porter de nonce. npm testverrouille ces protections (tests/security.test.ts).
Roles
admin > moderator > editor are staff; member is an adhérent linked to a
members row via users.memberId. Capability matrix is in src/lib/rbac.ts.
Docker notes
- Migrations + optional seed run on container start via
docker-entrypoint.sh. npm run build:scriptsbundlesmigrate/seedintodist/*.cjsso the runtime image needs no dev dependencies.- The standalone server binds
HOSTNAME=0.0.0.0,PORT=3000.
Logo
public/assets/logo.svg is a brand-colour recreation; swap in the official asset
when available (referenced as /assets/logo.svg).