Přeskočit na obsah

Komunitní fórum — vývojářská reference

Komunitní fórum — vývojářská reference

Section titled “Komunitní fórum — vývojářská reference”

Sencai komunitní fórum je postavené jako tenký orchestrační overlay nad upstream Lemmy Docker imagemi (sencai-forum), před kterým sedí účelově postavený Keycloak SSO bridge (forum-connector). Pohled z pozice koncového uživatele viz Komunitní fórum.

RepoRole
sencai-forumLemmy backend + lemmy-ui + PostgreSQL + pict-rs + oauth2-proxy, theme/translation overlay, bootstrap skripty. Žádný fork Lemmy kódu.
forum-connectorFastify sidecar (port 4181) — JIT (just-in-time) Keycloak↔Lemmy account provisioning, KC role → Lemmy admin sync, polling nepřečtených notifikací, org → community auto-provisioning.
  • Lemmy 0.19.x (Rust), ne Discourse/NodeBB — menší paměťová stopa (~200 MB vs. 2 GB+), PostgreSQL sedí na existující vzor (cloud-connector-db taky běží na Postgres).
  • oauth2-proxy sidecar pro SSO — Lemmy nemá oficiální OIDC plugin; oauth2-proxy chrání celý UI origin, takže se nic nedostane k Lemmy bez Keycloak session.
  • Federace je vypnutá (federation.enabled = false v lemmy.hjson) — menší útočná plocha, žádná moderation-policy komplexita. Pokud by se v budoucnu znovu zapnula, byl by potřeba domain allowlist.
  • Dockerfile customizuje jen lemmy-ui — backend, pict-rs, Postgres i oauth2-proxy běží z upstream images beze změn. Custom image je FROM dessalines/lemmy-ui → COPY theme → RUN patch translations — žádný Rust/Node build.
Browser → Traefik → forum-oauth2-proxy (4180) → forum-connector (4181) → forum-ui (1234)

forum-oauth2-proxy’s upstreams config ukazuje na forum-connector:4181 místo přímo na forum-ui:1234 (F4.FORUM.02) — forum-connector interně proxuje všechno kromě /jit-register//health rovnou na forum-ui přes @fastify/http-proxy, takže zbytek forum provozu je nedotčen.

Přímá volání mimo oauth2-proxy už nefungují (SEC 0d.4, 2026-07-16)

Section titled “Přímá volání mimo oauth2-proxy už nefungují (SEC 0d.4, 2026-07-16)”

/jit-register dřív důvěřoval X-Auth-Request-*/X-Forwarded-* hlavičkám bez ověření, že request skutečně prošel přes oauth2-proxy — protože forum-connector sdílí sencai-net s ~60 dalšími kontejnery, kterýkoliv z nich mohl tyhle hlavičky zfalšovat přímým voláním http://forum-connector:4181/jit-register. Route teď vyžaduje a kryptograficky ověřuje Keycloak ID token, který oauth2-proxy posílá přes Authorization: Bearer (set_authorization_header = true) — viz src/services/kc-token-verify.ts. Ruční curl s vloženými X-Auth-Request-* hlavičkami teď vrátí 401.

Na první návštěvě uživatele forum-connector zavolá Lemmy POST /api/v3/user/register s vygenerovaným interním heslem — žádný ruční admin krok. Mapování Keycloak↔Lemmy drží Strapi content-type forum-user-credential (sencai.space/src/api/forum-user-credential/):

PoleTypPoznámka
kc_substring, uniqueKC claim preferred_username, použitý jako stabilní identifikátor (viz níže) — ne OIDC claim sub
lemmy_usernamestringSanitizovaný Lemmy username
encrypted_passwordtext, private: trueAES-256-GCM, formát iv:authTag:ciphertext:salt
is_lemmy_adminbooleanMirror aktuálního Lemmy is_admin stavu — viz KC role sync níže, zdroj pravdy je vždy KC role

x-sencai-classification: tenant-private — tento content-type nese uživatelské kredence.

Proč preferred_username místo sub: oauth2-proxy’s set_xauthrequest nikdy neposílá sub jako samostatnou hlavičku; X-Auth-Request-User je nakonfigurován jako preferred_username mapper. Přejmenování tohoto identifikátoru později by vyžadovalo zpětnou migraci existujících záznamů.

Registrace předpokládá bez captcha/schválení — Lemmy registration_mode/captcha musí být vypnuté, aby self-service JIT flow mohl dokončit registraci v jednom volání. Pokud se registration_mode v lemmy.hjson změní, /jit-register přestane fungovat tiše (jiná Lemmy chyba, ne success) — zkontrolovat tohle nastavení jako první krok při debugování.

KC role forum-admin → Lemmy admin sync (F4.FORUM.03)

Section titled “KC role forum-admin → Lemmy admin sync (F4.FORUM.03)”

Na každé JIT registraci/loginu syncForumAdminRole() ověří, jestli přihlašovaný uživatel má v Keycloaku aktuálně přiřazenou realm roli forum-admin, a podle toho sesynchronizuje Lemmy is_admin flag + mirror pole forum-user-credential.is_lemmy_admin.

Proč Keycloak Admin API, ne oauth2-proxy hlavičky: oidc_groups_claim nikdy není nastaven (default plochý claim groups), ale realm_roles protocol mapper na KC klientu sencai-forum dává role do vnořeného claimu realm_access.roles — oauth2-proxy do něj nevidí, takže X-Auth-Request-Groups role nikdy nenese. pass_access_token/set_authorization_header jsou navíc obě false i pro syrový KC token (záměrně — syrový token by se ve většině případů neměl dostat na drát mezi oauth2-proxy a upstream). src/services/keycloak-client.ts místo toho volá Keycloak Admin API přímo (password grant, admin-cli, master realm — stejný vzor jako sencai.space/src/utils/keycloak-sync.ts), takže vždy vidí aktuální přiřazení role, ne co bylo zapečené do tokenu, který se ještě nerefreshnul.

Flow: hasRealmRole() (composite endpoint, takže se počítá i role zděděná přes group/default-roles composite) → pokud se výsledek liší od is_lemmy_admin, getPersonByUsername()setAdmin() (přihlášený jako bootstrap admin, LEMMY_ADMIN_USERNAME/LEMMY_ADMIN_PASSWORD, JWT cachovaný v paměti s auto-refresh na 401) → strapiClient.setLemmyAdminFlag() → audit forum.user.admin-promoted/forum.user.admin-demoted. Každý krok je best-effort — chyba na KC Admin API volání nebo chybějící Lemmy person se jen zaloguje a přeskočí, nikdy nezhatí samotný login/registraci. Sesazení role je líné — projeví se až při dalším loginu uživatele, ne real-time.

Dva reálné bugy byly nalezeny a opraveny při živém testování téhle funkce (žádný z nich nikdy předtím nebyl ověřen proti skutečnému Lemmy): setAdmin() volalo neexistující POST /api/v3/user/admin (skutečný endpoint v Lemmy 0.19.8 je POST /api/v3/admin/add), a getPersonByUsername() četlo admin flag ze špatného pole odpovědi (person_view.person.admin místo skutečného person_view.is_admin).

Lemmy 0.19 nemá webhook/event-hook mechanismus — jediná cesta, jak se dozvědět “někdo odpověděl/zmínil tohoto uživatele”, je periodický polling jeho jménem. src/services/notification-poller.ts běží ve stejném procesu jako HTTP server (spuštěn po úspěšném app.listen()), ne jako samostatná deploy jednotka.

Cyklus (výchozí 60s, FORUM_NOTIFICATION_POLL_INTERVAL_MS): pro každý záznam forum-user-credential dešifruj heslo → přihlas se → getUnreadCounts() (přeskoč hned, pokud jsou obě čísla nula, nejčastější případ). E-mail příjemce se čte přímo z Lemmy (GET /api/v3/site s uživatelovým vlastním JWT) místo aby se ukládal podruhé do forum-user-credential. Každá nepřečtená reply/mention se publikuje na notification.events (routing keys forum.reply/forum.mention) a označí se jako přečtená na Lemmy až po úspěšném publish — výpadek RabbitMQ nechá položku nepřečtenou a příští cyklus ji zkusí znovu.

Dedup je Lemmy’s vlastní read flag — žádné samostatné “last seen id” bookkeeping ve Strapi. Audit je jeden agregovaný forum.notification.forwarded event za cyklus (ne jeden per notifikaci), protože chatty fórum by jinak generovalo stovky nízko-hodnotných audit řádků.

Org → Lemmy community auto-provisioning (F4.FORUM.05)

Section titled “Org → Lemmy community auto-provisioning (F4.FORUM.05)”

src/consumers/organisation-consumer.ts běží samostatné RabbitMQ spojení ve stejném procesu (FORUM_ORG_COMMUNITY_CONSUMER_ENABLED=false ho vypne), konzumuje organisation.forum-community-requested z existující direct exchange organisation s dedikovaným routing key (záměrně ne znovupoužitím create, které by event fan-outovalo i na nesouvisející KC-group-sync consumer navázaný na stejný routing key).

Pro každou zprávu: odvoď Lemmy-safe community name ze slugu organizace → lemmyClient.createCommunity() (idempotentní — konflikt community_already_exists spadne na getCommunityByName() místo throw, takže opakovaná zpráva nikdy nevytvoří duplicitní community) → POST /api/organisations/:id/forum-community-callback na sencai.space, autentizováno dedikovaným FORUM_CONNECTOR_SERVICE_SECRET (ne stejný token, jaký strapi-client.ts používá pro zápisy forum-user-credential — stejný vzor separace privilegií jako cloud-connector’s status-callback) → audit forum.community.provisioned.

Retry: manuální ack/nack, header-based retry count, exponenciální backoff (5s * 2^attempt, max 5 pokusů), poté 14denní TTL fallback queue (forum-connector.organisation.forum-community-requested.fallback) místo zahození zprávy. Idempotence createCommunity() dělá každý retry bezpečný i když community už byla vytvořena a selhal jen callback krok.

GotchaPoznámka
local_site.registration_mode musí být OpenLemmy 0.19 defaultuje tento DB sloupec na RequireApplication při prvním initu — je to nezávislé na nastaveních captcha/require_application v lemmy.hjson a nejde ho natrvalo přednastavit přes hjson soubor. Opraveno scripts/configure-site-settings.sh (PUT /api/v3/site), spustit jednou po prvním startu.
local_site.federation_enabled ignoruje lemmy.hjson’s federation.enabled: falseDvě nezávislá nastavení — Lemmy defaultuje DB sloupec na true při prvním initu bez ohledu na hjson soubor. Stejný skript configure-site-settings.sh je nutno spustit i na existující DB, ne jen při novém initu.
Kolize @fastify/cors + @fastify/http-proxy routecors vždy registruje catch-all OPTIONS preflight route; http-proxy s prefix: '/' se snaží zaregistrovat stejnou route → server se vůbec nespustí. Opraveno vyloučením OPTIONS z http-proxy’s httpMethods. Žádný existující test necvičil index.ts samotný, dokud tohle nebylo nalezeno živě.
Cookie jméno jwtMusí zůstat přesně jwt — je to jméno cookie, které lemmy-ui samo nastavuje po nativním loginu a čeká ho na SSR straně.
Rate limiting /jit-register je per KC identita, ne per IPKaždý reálný request dorazí z jediné kontejnerové IP forum-oauth2-proxy — IP-only klíč by omezil všechny uživatele jedním sdíleným limitem. Default 10 req/min per identitu (kc_sub/preferred_username).
KomponentaPort
forum-connector4181
forum-oauth2-proxy4180
Lemmy backend API8536
PostgreSQL (forum)5435
  • Fleet agent, Marketplace, Audit trail — další content-type-backed subsystémy sledující stejný vzor dedikované stránky
  • notification-service — konzumuje forum.reply/forum.mention z notification.events
  • Kořenový CLAUDE.md — plná service map a konvence RabbitMQ exchange