mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-16 19:56:57 +02:00
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
118 lines
5.7 KiB
Markdown
118 lines
5.7 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.
|
|
- `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`).
|