Files
PleinR/CLAUDE.md
T
Claude 19d1104ab0 Connexion Facebook / LinkedIn en OAuth depuis le backoffice
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
2026-07-24 23:00:08 +00:00

4.5 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) 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

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 auth() 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() sont asynchrones (accès base).
  • 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.

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.

public/assets/logo.svg is a brand-colour recreation; swap in the official asset when available (referenced as /assets/logo.svg).