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.
Dva repozitáře, jedna funkce
Section titled “Dva repozitáře, jedna funkce”| Repo | Role |
|---|---|
sencai-forum | Lemmy backend + lemmy-ui + PostgreSQL + pict-rs + oauth2-proxy, theme/translation overlay, bootstrap skripty. Žádný fork Lemmy kódu. |
forum-connector | Fastify 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. |
Klíčová rozhodnutí (sencai-forum)
Section titled “Klíčová rozhodnutí (sencai-forum)”- 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-dbtaky 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 = falsevlemmy.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.
Request chain
Section titled “Request chain”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.
JIT user provisioning (F4.FORUM.02)
Section titled “JIT user provisioning (F4.FORUM.02)”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/):
| Pole | Typ | Poznámka |
|---|---|---|
kc_sub | string, unique | KC claim preferred_username, použitý jako stabilní identifikátor (viz níže) — ne OIDC claim sub |
lemmy_username | string | Sanitizovaný Lemmy username |
encrypted_password | text, private: true | AES-256-GCM, formát iv:authTag:ciphertext:salt |
is_lemmy_admin | boolean | Mirror 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).
Forum notification poller (F4.FORUM.04)
Section titled “Forum notification poller (F4.FORUM.04)”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.
Známé gotchas
Section titled “Známé gotchas”| Gotcha | Poznámka |
|---|---|
local_site.registration_mode musí být Open | Lemmy 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: false | Dvě 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 route | cors 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 jwt | Musí 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 IP | Kaž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). |
| Komponenta | Port |
|---|---|
| forum-connector | 4181 |
| forum-oauth2-proxy | 4180 |
| Lemmy backend API | 8536 |
| PostgreSQL (forum) | 5435 |
Související služby
Section titled “Související služby”- Fleet agent, Marketplace, Audit trail — další content-type-backed subsystémy sledující stejný vzor dedikované stránky
notification-service— konzumujeforum.reply/forum.mentionznotification.events- Kořenový
CLAUDE.md— plná service map a konvence RabbitMQ exchange