Commit Graph
15 Commits
Author SHA1 Message Date
MichaelandClaude Opus 5 146250f973 Modération : la diffusion réseaux suit la date de publication
Le bloc « Diffusion demandée » du formulaire « Valider » annonçait une
publication « dès la validation », alors qu'une promotion validée avec une
date future passe en `scheduled` et n'est diffusée qu'à l'échéance, par
`releaseDuePromotions()`. Le texte décrit maintenant les deux cas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS61rkgTUGgPYH88tuVx9U
2026-09-14 05:44:01 +00:00
Claude cf8cf5c8e2 Promotions : le logo revient à gauche du nom de l'adhérent
L'angle de la carte est rendu à la catégorie ; la pastille du commerce se
place devant son nom, en pied de carte, là où on lit déjà qui propose l'offre.
La période de validité passe sous le nom, dans le même bloc.

Appliqué à la carte d'accueil, à l'aperçu en direct de l'espace adhérent et à
la carte de modération. Retiré de la carte de la fiche adhérent : elle n'y
affiche pas de nom de commerce, et le logo figure déjà en tête de page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 14:22:03 +00:00
Claude d99ee7ec07 Promotions : logo de l'adhérent dans l'angle des cartes
Le logo du commerce identifie l'offre avant même qu'on lise le nom. Il est
posé en pastille ronde dans l'angle haut-gauche de la carte, la catégorie se
décalant juste à côté sur la même ligne.

- `src/components/MemberAvatar.tsx` : logo en `contain` sur fond blanc (les
  logos d'adhérents sont des marques sur fond clair, un `cover` les rognerait),
  repli sur les initiales colorées quand la fiche n'a pas de logo.
- Appliqué à la carte d'accueil, à la carte de la fiche adhérent, à l'aperçu
  en direct de l'espace adhérent et à la carte de modération : même composition
  partout, l'aperçu montre donc bien ce que verra le visiteur.
- `getLivePromotions` remonte `members.logo_url` ; la page espace le passe au
  formulaire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 14:02:53 +00:00
Claude e980ab6e04 Promotions : publication programmée et affichage limité à la période
Deux comportements demandés :

1. Une promotion n'apparaît sur le site que pendant sa période de validité.
   Les lectures publiques passent par `VISIBLE_PROMO` (`src/lib/queries.ts`) :
   statut `live` **et** fenêtre `starts_on` / `ends_on`, journée calculée en
   Europe/Paris pour qu'un conteneur en UTC ne retire pas une offre deux
   heures trop tôt. Le statut n'est pas touché : hors période, le backoffice
   explique pourquoi l'offre n'est pas visible (`visibilityNote`) et elle
   revient d'elle-même si les dates changent.

2. La publication peut être programmée. `promotions.publish_at` : l'adhérent
   propose une date, le modérateur la garde, la déplace ou la vide dans le
   formulaire « Valider ». Avec une échéance future la promotion passe au
   statut `scheduled` — rien n'est visible, rien n'est diffusé — et
   `releaseDuePromotions()` fait la bascule à l'heure dite, site et réseaux
   ensemble. Un bouton « Publier maintenant » court-circuite l'attente.

Mécanique :
- `src/lib/promo-schedule.ts` (pur, testé) : conversion `datetime-local` ⇄
  heure de l'association, changements d'heure compris, et refus d'une saisie
  illisible plutôt qu'une publication immédiate involontaire.
- `src/lib/promo-publish.ts` reprend `publishPromoShares()` : deux appelants
  désormais, une action serveur et la boucle de libération, qui n'a pas de
  requête et ne peut donc pas appeler `revalidatePath()`. Idem pour
  `logActivity()` dans `src/lib/activity-log.ts`.
- La bascule `scheduled → live` est un `UPDATE … RETURNING` filtré sur le
  statut : atomique, donc pas de double publication même à plusieurs
  instances, en plus de la garde existante sur `social_posts`.
- Déclencheur : `src/instrumentation.ts`, boucle d'une minute avec rattrapage
  au démarrage (`PROMO_SCHEDULER=off` la désactive). Le travail vit dans
  `instrumentation-node.ts`, écarté du bundle edge par `next.config.mjs` :
  Next compile aussi l'instrumentation pour le middleware, où `pg` ne peut
  pas être embarqué.

Migration 0016 : `promo_status += 'scheduled'`, `promotions.publish_at`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 11:17:49 +00:00
Claude 02c5378a47 Promotions : période de validité (date de début / date de fin)
Une promotion peut désormais porter une date de début et une date de fin,
toutes deux facultatives et indépendantes. Sans date, rien n'est affiché.

- Schéma : `promotions.starts_on` / `ends_on` (`date`), migration 0015.
- `src/lib/promo-validity.ts` (pur, testé) : `formatValidity()` produit
  « Valable du 1er au 15 mars 2027 », « Valable jusqu'au … », « Valable à
  partir du … » en repliant mois et année quand ils se répètent ;
  `formatValidityShort()` pour les lignes de méta ; `isRangeInvalid()` pour
  la validation.
- Espace adhérent : deux champs date, aperçu en direct de la phrase et
  blocage de l'envoi si la fin précède le début.
- `publishPromo` relit et revalide les bornes côté serveur.
- Affichage : carte d'accueil, fiche adhérent, liste de l'adhérent, carte de
  modération — et le message Facebook / LinkedIn, via le même formateur.
- L'ancien texte libre `valid_until` reste comme repli d'affichage pour les
  promotions de démonstration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 11:00:44 +00:00
Claude b0788d3f20 Durcissement : mot de passe actuel, sessions révocables, dépendances, CSP, tests
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
2026-07-25 10:18:04 +00:00
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
Claude 8bc1b17fc5 Diffusion réseaux pilotée par la validation de la promotion
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
2026-07-24 22:41:25 +00:00
Claude e3a4771edf Refonte du bloc infos pratiques, suspension des promos et publication réseaux
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
2026-07-24 22:20:51 +00:00
Michael 28ada88155 Add association meetings and GDPR settings 2026-07-17 17:47:24 +02:00
Michael b4c8967a3c Améliore les modales et les informations du site 2026-07-17 09:00:29 +02:00
Claude a4915b445d Cartes promo : cadre image carré 1:1 (adapté aux visuels Facebook)
Les visuels promo sont carrés (posts Facebook) : un cadre 1200/630 laissait des
bandes floues sur les côtés. Passage du cadre image en 1:1 partout (accueil,
fiche, aperçu espace adhérent, modération) → les images carrées remplissent
sans bords. Conseil d'upload : image carrée (1080×1080).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-27 21:18:18 +00:00
Claude 7037e8863b Cartes promo : zone image en aspect-ratio 1200/630 (nouvelle maquette)
Aligne les cartes promo sur la dernière refonte Claude Design : la zone image
passe d'une hauteur fixe à un ratio 1200/630, au-dessus du composant PromoImage
existant (remplissage flou + contain). Appliqué à l'accueil, la fiche adhérent,
l'aperçu de l'espace adhérent et la modération. Conseil d'upload : 1200×630.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-27 15:54:22 +00:00
MichaelandClaude Opus 4.8 03eca6e9ae feat: adaptive promo images with blurred fill
Replace cover-crop background with PromoImage component: sharp
contain image over a blurred cover copy. Whole image visible on
any aspect ratio, no flat letterbox bars. Applied to accueil,
adherent profile, backend promotions, member space preview +
upload dropzone.

Also retheme adherent logo frame (cream gradient + warm border)
instead of flat white.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:59:46 +02:00
Claude 7e22ec50d4 Implémente le site Plein R (Accueil + Backend) sur Next.js + Postgres + Docker
Application unique Next.js (App Router, TypeScript) servant :
- Accueil public : héro, recherche, métiers, promotions, mises à l'honneur
  (données issues de Postgres)
- Backend authentifié avec RBAC : tableau de bord, adhérents (CRUD), modération
  des promotions, administrateurs, espace adhérent (publication avec aperçu)

Technique :
- Drizzle ORM + drizzle-kit (migrations versionnées) sur driver pg
- Auth.js v5 (credentials, sessions JWT), rôles admin/moderator/editor/member
- Middleware de protection de /backend + gardes par rôle dans chaque vue
- Server actions pour toutes les mutations (vérif auth + capacité + revalidate)
- Design fidèle aux maquettes (design-system CSS, animations, responsive)

Conteneurisation :
- 1 conteneur applicatif (sortie standalone) + 1 conteneur Postgres externe
- docker-compose avec healthcheck ; migrations + seed au démarrage
- scripts migrate/seed bundlés (esbuild) pour un runtime sans devDeps

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 12:06:21 +00:00