mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-11 17:27:54 +02:00
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
129 lines
6.4 KiB
Markdown
129 lines
6.4 KiB
Markdown
# 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`) with `drizzle-kit` migrations
|
|
- Auth.js v5 (credentials provider, JWT sessions) — RBAC in `src/lib/rbac.ts`
|
|
- One app container + one Postgres container (`docker-compose.yml`)
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
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 via `getSession()` and `can()` before writing, then
|
|
`revalidatePath()`.
|
|
- **Access control**: `src/middleware.ts` gates `/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`, run `npm run db:generate` and commit the new
|
|
file under `drizzle/`.
|
|
|
|
## 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.ts` publie une promo sur la page Facebook (Graph API) ou
|
|
LinkedIn (Posts API). `src/lib/social-accounts.ts` gère la configuration : OAuth,
|
|
jetons, cibles. Réseau non configuré = case masquée.
|
|
- Les identifiants et jetons vivent en base (`social_accounts`), **chiffrés** via
|
|
`src/lib/crypto.ts` (AES-256-GCM, clé `SOCIAL_TOKEN_KEY` ou `AUTH_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). `saveSiteSettings` **saute**
|
|
cette clé : le formulaire Paramètres ne la contient pas et l'écraserait.
|
|
- Routes OAuth : `src/app/api/social/[network]/{connect,callback}`. Le `state`
|
|
anti-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_linkedin` sont choisis par l'adhérent,
|
|
ajustables par le modérateur dans le formulaire « Valider », puis figés
|
|
(`status !== 'pending'`).
|
|
- `publishPromoShares()` dans `backend/actions.ts` est le seul point de
|
|
publication. Elle ne lève jamais et ignore tout réseau ayant déjà une ligne
|
|
`social_posts` en `posted` : c'est la garde anti-republication, qui couvre
|
|
aussi le cycle suspension → remise en ligne.
|
|
- `retryPromoShare` ne 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`) remplace `auth()` partout : le rôle et le
|
|
rattachement adhérent sont relus en base à chaque requête, et
|
|
`users.session_version` invalide les jetons émis avant un changement de mot de
|
|
passe. N'appelez plus `auth()` 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 dans `app/layout.tsx`,
|
|
un HTML pré-généré ne pouvant pas porter de nonce.
|
|
- Les mots de passe temporaires (création d'adhérent, réinitialisation,
|
|
invitation staff) ne sont **jamais stockés** : l'action les renvoie et le
|
|
composant `OneTimeCredentials` les affiche une seule fois, sans redirection.
|
|
`users.must_change_password` seul persiste.
|
|
- Le seed ne crée en production que l'administrateur initial, avec un mot de
|
|
passe aléatoire affiché une fois dans les journaux (ou `SEED_ADMIN_PASSWORD`)
|
|
et un changement obligatoire à la première connexion. Les comptes de démo
|
|
(`changeme123`) exigent `SEED_DEMO=true`.
|
|
- Sessions JWT limitées à 7 jours (`auth.config.ts`), HSTS et suppression de
|
|
`X-Powered-By` dans `next.config.mjs`. Le port Postgres de `docker-compose`
|
|
n'est publié que sur `127.0.0.1`.
|
|
- `npm test` verrouille 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:scripts` bundles `migrate`/`seed` into `dist/*.cjs` so 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`).
|