Skip to content

Durcissement de la vitrine publique et des catégories d'annonce - #660

Open
Pokled wants to merge 2 commits into
mainfrom
feat/durcissement-vitrine
Open

Durcissement de la vitrine publique et des catégories d'annonce#660
Pokled wants to merge 2 commits into
mainfrom
feat/durcissement-vitrine

Conversation

@Pokled

@Pokled Pokled commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Contexte

Incident du 2026-09-01 : un compte member standard (nerti / spam SEO), sans aucune faille d'authentification ni élévation de privilège, a fait remonter un fil « Alsouq Alshabi » :

  1. en tête de la page d'accueil (widgets « fils récents » et « vitrine articles ») ;
  2. dans l'index de recherche global nodyx.org (network_index), propageable aux instances pairs.

Cause : n'importe qui peut écrire dans 📣 Annonces, et trois surfaces publiques trient les fils par fraîcheur, sans notion de rôle ni de curation.

Nettoyage de l'incident déjà fait à la main (compte + email + IP bannis, fil supprimé, annuaire purgé). Cette PR empêche la récidive.

Changements

Migration 117_durcissement_vitrine.sql

  • categories.post_min_role (défaut member, inerte — rétro-compatible pour les instances tierces)
  • threads.showcased_at (+ backfill des fils déjà is_featured)
  • users.last_seen_ip
  • verrou de la catégorie annonces sur admin
  • rognage des pseudos existants (garde anti-collision d'unicité)

Backend

  • POST /forums/threads403 CATEGORY_RESTRICTED si le rôle de l'auteur est sous le post_min_role de la catégorie
  • PATCH /forums/threads/:idis_featured passe sous le même verrou owner/admin que pin/lock (il était ouvert aux modérateurs par omission) et pose showcased_at
  • getFeatured / /instance/threads/showcase → ne renvoient que les fils explicitement mis en avant, plus par fraîcheur
  • /instance/threads/recent → masque les fils non-featured des catégories restreintes
  • scheduler.announceThreadsToDirectory → n'annonce à la fédération que les fils is_featured
  • PATCH /admin/categories/:id → accepte post_min_role (+ logAction)
  • POST /admin/members/:id/ban → le ban IP préfère last_seen_ip (vraie adresse captée au login) à registration_ip (toujours 127.0.0.1 depuis le tunnel), et renvoie ip_ban_applied au lieu d'un faux succès
  • registertrim() du username

Frontend

  • Nouveau sujet : le sélecteur ne propose que les catégories postables pour le rôle courant ; message clair sinon
  • Page fil : boutons épingler / verrouiller / promouvoir réservés owner/admin à l'affichage
  • Admin catégories : sélecteur « qui peut ouvrir un fil » + badge cadenas
  • Admin membres : retour honnête sur le ban IP
  • i18n fr + en à parité (clés forum.*, acat.*, amem.*)

Tests

  • forums.test.ts : +5 (catégorie restreinte 403 / admin OK / non-régression member ; is_featured mod→403, admin→200)
  • admin.test.ts : +2 (ip_ban_applied null quand pas d'IP publique / = last_seen_ip sinon)
  • durcissement-vitrine.test.ts : 6 invariants sur la migration (colonnes additives, verrou ciblé, backfill non destructif, garde anti-collision)
  • Suite complète : 984 tests verts, tsc clean, npm run check 0 erreur, 5 portes i18n vertes

Déploiement

Migration idempotente jouée au boot. Dry-run (transaction rollback) sur la prod OK : 3 colonnes ajoutées, annonces verrouillé, 17 fils featured rétro-datés, 1 pseudo rogné. Rollback code sans risque (colonnes inertes, aucun DROP).

Décisions ouvertes tranchées (voir §9 du CDC)

D1 verrou annonces dans la migration · D2 is_featured admin-only · D3 /threads/recent masque les restreintes · D4 critère fédération = is_featured · D5 last_seen_ip ajouté · D6 ménage rétroactif network_index reporté (script séparé)

…e publique

Un compte membre sans aucun privilège pouvait faire remonter un fil en tête
de la page d'accueil et dans l'index de recherche fédéré nodyx.org, en
postant simplement dans une catégorie d'annonces : les surfaces vitrine
n'avaient aucune notion de rôle ni de curation, elles triaient par fraîcheur.

- migration 117 : colonne categories.post_min_role (défaut 'member', inerte),
  threads.showcased_at, users.last_seen_ip ; verrou de la catégorie annonces ;
  rognage des pseudos existants (garde anti-collision).
- POST /forums/threads : refus 403 CATEGORY_RESTRICTED si le rôle de l'auteur
  est sous le post_min_role de la catégorie.
- PATCH /forums/threads/:id : is_featured passe sous le même verrou admin que
  pin/lock (il était ouvert aux modérateurs par omission) et pose showcased_at.
- getFeatured / /instance/threads/showcase : ne renvoient que les fils
  explicitement mis en avant (showcased_at), plus par fraîcheur.
- /instance/threads/recent : masque les fils non-featured des catégories
  restreintes (un brouillon d'annonce ne fuite plus).
- scheduler : n'annonce à l'annuaire fédéré que les fils is_featured.
- admin : PATCH /categories accepte post_min_role ; ban IP préfère la vraie
  adresse (last_seen_ip) et renvoie ip_ban_applied au lieu de mentir.
- register : trim() du username.

CDC : SPECS/NODYX_DURCISSEMENT_VITRINE_CDC.md
- nouveau sujet : le sélecteur ne propose que les catégories où le rôle de
  l'utilisateur autorise la publication ; message clair si aucune n'est ouverte
  ou si le back renvoie CATEGORY_RESTRICTED.
- page fil : les boutons épingler / verrouiller / promouvoir ne s'affichent
  qu'aux owner/admin (ils étaient montrés aux modérateurs pour un 403).
- admin catégories : sélecteur « qui peut ouvrir un fil » + badge cadenas.
- admin membres : après un ban, indique si l'IP a réellement été bannie
  (ou pourquoi non), au lieu d'un succès générique trompeur.
- i18n : clés forum.* / acat.* / amem.* ajoutées en fr et en.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant