feat: Reseller-Rolle + SUPER_ADMIN-Mandantenübersicht

Mandantenfähigkeit ausgebaut:
- Neue Rolle RESELLER (company_id NULL); companies.reseller_id + is_active
- RLS-Erweiterung (Migration 0034): companies/users zusätzlich auf app.reseller_id
  gefenced → Reseller sieht/verwaltet DB-seitig nur eigene Firmen, keine
  personenbezogenen Zeit-/Abwesenheitsdaten (DSGVO: nur Verwaltung)
- get_current_user setzt app.reseller_id + Bypass aus für RESELLER
- tenant_service: Firma + Erst-Admin (Einladung), Übersicht mit Kennzahlen
- Router /reseller/* (Self-Service) und /admin/* (SUPER_ADMIN: Mandanten + Reseller)
- Login-Sperre bei deaktiviertem Mandanten
- Frontend: TenantsPage (/admin/tenants), eigene ResellerCompaniesPage (/reseller),
  rollenbasierte Login-Weiterleitung, Nav "Mandanten" für SUPER_ADMIN
- 4 neue Tests inkl. Cross-Reseller-RLS-Isolation; 172/172 grün

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 01:15:51 +02:00
co-authored by Claude Opus 4.8
parent da4745013e
commit fcda813ba6
18 changed files with 1198 additions and 8 deletions
+11 -3
View File
@@ -18,9 +18,16 @@ TestSessionLocal = async_sessionmaker(test_engine, class_=AsyncSession, expire_o
_BYPASS = "COALESCE(current_setting('app.bypass_rls', true), 'off') = 'on'"
_CID = "company_id = NULLIF(current_setting('app.company_id', true), '')::uuid"
_IID = "id = NULLIF(current_setting('app.company_id', true), '')::uuid"
_RID = "reseller_id = NULLIF(current_setting('app.reseller_id', true), '')::uuid"
# Reseller darf User seiner eigenen Firmen verwalten (vgl. Migration 0034)
_USER_RESELLER = (
"company_id IN (SELECT id FROM companies WHERE "
"reseller_id = NULLIF(current_setting('app.reseller_id', true), '')::uuid)"
)
def _rls_using_cid(): return f"({_BYPASS} OR {_CID})"
def _rls_using_iid(): return f"({_BYPASS} OR {_IID})"
def _rls_using_cid(): return f"({_BYPASS} OR {_CID})"
def _rls_using_iid(): return f"({_BYPASS} OR {_IID} OR {_RID})"
def _rls_using_users(): return f"({_BYPASS} OR {_CID} OR {_USER_RESELLER})"
def _rls_using_join(): return (
f"({_BYPASS} OR user_id IN (SELECT id FROM users WHERE {_CID}))"
)
@@ -55,7 +62,8 @@ async def _apply_rls(conn) -> None:
for sql in enable("companies", _rls_using_iid()):
await conn.execute(text(sql))
for table in _COMPANY_COL_TABLES:
for sql in enable(table, _rls_using_cid()):
using = _rls_using_users() if table == "users" else _rls_using_cid()
for sql in enable(table, using):
await conn.execute(text(sql))
for table in _USER_JOIN_TABLES:
for sql in enable(table, _rls_using_join()):