Renouveler un jeton demandait de deviner que « Reconnecter » servait à ça, et
les outils Meta / LinkedIn étaient enfouis sous « Comment obtenir ces
identifiants ? ». Chaque réseau connecté porte maintenant un bloc dédié :
- Un bouton « Renouveler le jeton <réseau> », qui relance l'échange OAuth
complet (jeton court → longue durée → jeton de page).
- Les deux liens utiles à ce moment-là, chacun avec ce qu'il sert à faire :
débogueur de jeton et outil de jetons d'accès pour Facebook, jetons de
l'application et documentation de durée de vie pour LinkedIn.
- Le bloc passe en ambre quand le jeton est mort, expiré ou proche de l'être.
« Reconnecter » disparaît : il faisait exactement la même chose que le nouveau
bouton, à dix pixels de distance. Une fois connecté, il ne reste que
« Déconnecter » ; « Connecter » n'apparaît plus que pour la première liaison.
Les bannières d'alerte nomment désormais le bouton qui existe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
Un jeton de page Facebook n'a pas de date d'expiration, donc `expiryStatus`
renvoyait « never » et aucune alerte ne pouvait se déclencher. Mais il meurt
quand même : mot de passe du compte changé, rôle d'administrateur perdu sur la
page, autorisation retirée, révocation par Meta. On ne l'apprenait qu'au moment
où une publication échouait.
- `checkTokenHealth()` interroge la plateforme (`/me` côté Graph, les ACL
d'organisation côté LinkedIn) et rend un verdict motivé.
- L'écran Réseaux sociaux contrôle à chaque affichage : pastille « Jeton
expiré », bandeau rouge et réponse brute de la plateforme quand il est mort,
ligne verte de confirmation sinon.
- Le verdict est persisté (`social_accounts.last_check_*`) : le tableau de bord
alerte via `tokenHealthCached()` (fenêtre de 6 h) sans appeler la plateforme à
chaque affichage, et croise ce signal avec la dernière tentative de
publication refusée pour motif d'autorisation.
- Bloc « Outils pour inspecter un jeton » dans l'aide de chaque réseau :
débogueur de jeton, outil de jetons d'accès, explorateur d'API Graph,
documentation des jetons de longue durée — en précisant que le bouton
Connecter fait déjà l'échange court → longue durée → jeton de page, et qu'il
n'y a aucun jeton à copier à la main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
Cliquer sur « Connecter » sans avoir enregistré l'URL publique du site
renvoyait une erreur, et la redirection retombait sur https://0.0.0.0 :
la directive upgrade-insecure-requests était posée même sur une page
servie en HTTP, le navigateur basculait donc la navigation vers un
https:// inexistant.
- `publicBaseUrl()` : réglage `site_public_url`, variables d'environnement,
puis origine de la requête (X-Forwarded-Host / X-Forwarded-Proto). L'adresse
de retour OAuth et le lien des publications l'utilisent ; `seo-server`
s'appuie dessus au lieu de dupliquer la déduction.
- `src/lib/site-url.ts` : déduction pure et testée de l'origine, HTTP par
défaut pour localhost et les IP nues, HTTPS pour un nom de domaine.
- Middleware : `upgrade-insecure-requests` uniquement quand la page est servie
en HTTPS.
- Page Réseaux : l'adresse détectée est annoncée comme valeur par défaut, avec
le rappel que Facebook et LinkedIn exigent du HTTPS sur un domaine.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
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
Connecter un réseau social imposait de poser NEXT_PUBLIC_SITE_URL dans
docker-compose et de redémarrer le conteneur. L'écran Réseaux sociaux porte
désormais le champ, pré-rempli avec l'adresse par laquelle l'administrateur
consulte la page.
- Nouveau réglage `site_public_url` ; `siteUrl()` lit la base puis retombe sur
les variables d'environnement, donc les déploiements actuels ne bougent pas.
- `siteUrl()`, `redirectUri()`, `authorizeUrl()`, `promoLink()` et
`buildPromoMessage()` deviennent asynchrones.
- Seule l'origine est conservée : le schéma est ajouté s'il manque, un chemin
est retiré, et une adresse invalide revient sur l'écran avec un message au
lieu d'une page d'erreur brute.
- `saveSiteSettings` saute cette clé : le formulaire Paramètres ne la contient
pas et l'aurait écrasée à chaque enregistrement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
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