feat(identity,tenancy): invite-only login, companies and forced RLS - #31
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ion access control Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…LS with integration proofs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…for #4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
This was referenced Sep 23, 2026
- sign-up requires the invitation id (link) plus the invited e-mail – no takeover by address alone - one company per user (unique index), deterministic membership lookup, actor from membership - organization plugin accepts only admin/clerk roles - configurable client-IP source for the auth rate limit; local secret refused in production - invite page: zod input, 404 for clerks, shows the invitation link; signup needs the link - tests: wrong/missing invitation id, foreign set-active/list-members, last admin, roles - docs: operations (rate limit/proxy, recovery), data model, exceptions register (admin plugin) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
8 of 15 tasks
…secret The image sets NODE_ENV=production, so the previous check blocked the local compose stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Owner
Author
Frischer Review + Security-Review (unabhängiger Subagent) – Befunde und AuflösungReview nach
Admin-Plugin: bleibt laut ADR D6 eingebunden, niemand hält die Plattform-Admin-Rolle (Endpunkte lehnen jeden ab, getestet) – als Ausnahme mit Ablauf „#30" im Register eingetragen. Verdict des Reviewers: nach Behebung mergebar, menschliche Freigabe nötig (Auth + Tenancy + Migration). Generated by Claude Code |
7 tasks
Fluory
marked this pull request as ready for review
September 23, 2026 06:13
7 tasks done
6 of 15 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warum
Fixes #4 · Teil von Epic #2 · gestapelt auf #21 (Basis
claude/chore-app-skeleton-3).Arbeitsstand
withTenantunter erzwungener RLS.pnpm verifylokal grün; CIcheck+compose-smokegrün.Was ist passiert (Klartext)
Mitarbeitende melden sich jetzt mit E-Mail und Passwort an – aber nur, wenn sie eingeladen wurden. Eine Administratorin legt auf
/inviteeine Einladung an und gibt den Link weiter; ohne diesen Link entsteht kein Konto, auch wenn jemand die richtige Adresse kennt. Jede Person gehört zu genau einer Firma und hat dort die Rolle „Administration" oder „Sachbearbeitung".Die Firmendaten sind doppelt getrennt: Der Code fragt nur im Rahmen der eigenen Firma ab (
withTenant), und die Datenbank selbst verweigert alles andere per Row-Level Security – auch wenn im Code ein Filter fehlen würde. Tests beweisen das mit zwei Firmen, über die normalen Funktionen und über rohes SQL. Anmeldeversuche sind begrenzt (Rate Limit in der Datenbank).Plan-Pflicht (SYSTEM.md §4)
Impact Manifest
identity(Better Auth,authorize(), Einladung,getActor),tenancy(withTenant, RLS),requests(erste Mandantentabelle),db(Schemaauth+app.requests, Migrationen 0001/0002),app(Login, Registrierung per Link, Einladung,/api/auth/*),config(Auth-Secret,APP_ENV, IP-Quelle),src/seed.ts.auth(Better-Auth-Tabellen, UUID-IDs, Uniquemember.user_id) ohne RLS (Ausnahmenregister);app.requestsmit ENABLE + FORCE RLS, Policycompany_id = nullif(current_setting('app.company_id', true), '')::uuid; Endpunkte/api/auth/*; Seiten/login,/signup?invitation=…,/invite.authorize(); Integration überauth.handler(HTTP) und rohes SQL alsapp_rw.better-auth1.7.5 und@better-auth/drizzle-adapter1.7.5 (MIT, Peers passen); Schema per Better-Auth-CLI (auth@1.7.5 generate) erzeugt; abgelehnte Registrierung liefert bewusst dieselbe generische Antwort wie eine erfolgreiche (Anti-Enumeration) – „abgelehnt" wird über die Wirkung geprüft; nach verweigertemset-activeleert Better AuthactiveOrganizationId.docker compose down -v.Akzeptanzkriterien → Nachweis
identity.test.ts: kein Nutzer, kein Token, kein Login; auch mit falscher/fremder Einladungs-IDidentity.test.ts(„session carries their active company and role")withTenantsetztapp.company_idtransaktionslokal; Repositories nur mit Tenant-Kontexttenancy.test.ts(Pool-Reuse mitmax: 1, Repository ohne Tenant →MissingTenantError, Nicht-UUID abgelehnt); Typ-BrandTenantTxapp_rwtenancy.test.ts(Repository, rohes SQL mit/ohne Kontext)tenancy.test.ts(Insert und Update auf fremdecompany_id→ RLS-Fehler)admin/clerk;authorize()verweigert Admin-Aktionen für Clerksauthorize.test.ts(test-first) + HTTP-Test Plugin-Einladung durch Clerk → 403package.json;identity.test.ts(429 + Zeile inauth.rate_limit); Konfigurationauth.tsGeändert
src/features/identity/:auth.ts(Better Auth: Einladungs-Gate mit ID, Session-Hook, Rollen-Hooks, IP-Quelle),access.ts,authorize.ts(+Test),companies.ts(Firma anlegen, einladen),actor.ts.src/features/tenancy/with-tenant.ts,src/features/requests/repository.ts.src/db/schema/{auth,app,index}.ts, Migrationen0001_identity_tenancy.sql(generiert, angepasst),0002_auth_grants_force_rls.sql(Grants, FORCE RLS, Unique-Mitgliedschaft).src/app/:/api/auth/[...all],/login,/signup,/invite, Startseite,_server/runtime.ts,_components/auth-form.tsx;src/seed.ts(pnpm seed:demo).src/config/env.ts(+Tests):BETTER_AUTH_*,APP_ENV,AUTH_IP_HEADERS,AUTH_TRUSTED_PROXIES;.env.example,compose.yaml,vitest.config.ts.tests/integration/{identity,tenancy}.test.ts,helpers/stack.ts.docs/technical/data-model.md(neu, mit Klassifikation), Architekturkarte (Status, Ausnahme Admin-Plugin),operations.md(Rate Limit/Proxy, Einladung, Recovery), CHANGELOG.Nachweis (SYSTEM.md §11)
verify:changed: grünverify: grün – lokal: lint, typecheck, 23 Unit + 32 Integrationstests gegen echtes PostgreSQL 17, depcruise 0 Verstöße, build, audit 0 high; CIcheck+compose-smokegrün: https://github.com/Fluory/RequestFlow/actions/runs/35825480993verify:full/ E2E-Spec: nicht betroffenpnpm seed:demo→ Standalone-Server: Admin-Login 200, Startseite zeigt Firma + „Mitarbeitende einladen",/inviteals Admin 200, als Clerk 404, anonym → Redirect/login.Doku-Entscheidung (genau eine)
docs/technical/data-model.md(neu, mit Datenklassifikation),docs/technical/operations.mddocs/technical/architecture.md(Status identity/tenancy/requests, Ausnahmenregister)[Unreleased](sichtbares Feature oder Verhalten – im selben PR, nie „später")Entferntes oder Umbenanntes: nichts entfernt
Dateigrößen und neue Bausteine (SYSTEM.md §7)
Dateien über 500 Zeilen im Diff (Ausnahmen: generierter Code, Lockfiles, Fixtures, Migrationen, Schemas, Ressourcen, Doku, Konfiguration):
Über 800 Zeilen mit neuer Fachlogik oder über 1000 Zeilen (P1/P2): nicht betroffen
Neue Shared-Komponente, Utility-Datei, Adapter oder fachlicher Service:
identityundtenancysind Skelette aus chore(app): TS app skeleton, docker compose and verify commands #3Subagent-Einsätze
review-prund.claude/rules/security.md– 1 Blocker, 3 Important, 4 Notes, alle bearbeitet.Risiken / offene Punkte
🤖 Generated with Claude Code
https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1