Zum Hauptinhalt springen

Authentifizierung & Rollen (RBAC)

Authentifizierung​

Roestify nutzt Auth.js (NextAuth v5 beta 30) mit dem @auth/drizzle-adapter.

Providers​

  • Credentials (Email/Password) — bcryptjs-Hashing, immer aktiv
  • Google OAuth — Bedingt aktiv wenn AUTH_GOOGLE_ID + AUTH_GOOGLE_SECRET gesetzt

Session-Strategie​

JWT-basiert (nicht Datenbank-Sessions). Custom Claims im Token:

{
user: {
id: string // User-ID
email: string
role: "admin" | "roester" | "packer" | "buero" | "readonly"
tenantId: string // Aktiver Tenant
locale: string // de | en
}
}

Die Rolle wird bei jedem JWT-Refresh aus der profiles-Tabelle geladen (nicht gecacht).

Konfiguration​

  • Datei: src/server/auth/index.ts
  • Adapter: @auth/drizzle-adapter (Tabellen: users, accounts, sessions, verificationTokens)
  • Login-Seite: /login
  • Onboarding: /onboarding (Erstregistrierung)

Event-Hooks​

  • createUser: Bei Erstregistrierung → Trial-Tenant + Profil erstellen ODER ausstehende Einladung akzeptieren
  • jwt: Rolle + tenantId aus DB laden
  • session: Custom Claims in JWT injizieren

Middleware​

middleware.ts schützt alle Routen:

  • Routen unter (protected) → Auth erforderlich
  • Öffentliche Routen: Login, Register, Password Reset, Landing, Display API
  • Locale-Erkennung via Accept-Language-Header

Rate Limiting​

In-Memory Rate Limiter (src/lib/rate-limit.ts) mit Sliding-Window-Ansatz:

AktionLimitFenster
Logo-Upload10 Requests/Minute/IP
Shopify OAuth10 Requests/Minute/User
Order-Form Auth-Code3 Requests/Stunde/E-Mail
Order-Form Auth-Code10 Requests/Stunde/IP
Shopify Sync3 Requests/2 Minuten/User

Auto-Cleanup alle 60 Sekunden (abgelaufene Einträge).

Rollen-System (5 Rollen)​

type Role = 'admin' | 'roester' | 'packer' | 'buero' | 'readonly';

Permission-Matrix​

RessourceAdminRösterPackerBüroReadonly
DashboardWWWWR
PlanungWW——R
Röst-LogWW——R
RohkaffeeWW——R
MaschinenWW——R
TonnenWWR—R
AbpackungW—W—R
ProfileWRRRR
B2B KundenW——WR
BestellungenW——WR
RechnungenW——WR
PreiseW——WR
FinanzenW——WR
RückverfolgungWWRWR
EinstellungenW————
Team & BillingW————
Shopify/DHLW————

W = Write (CRUD), R = Read-Only, — = Kein Zugriff

3-Level Enforcement​

1. Client — Sidebar + UI-Elemente ausblenden/deaktivieren
2. Server — tRPC protectedProcedure + Permission-Check (403 bei Verstoß)
3. Datenbank — RLS-Policies (letzte Verteidigungslinie)

tRPC-Implementierung​

// tRPC Context enthält Session mit Rolle
const { userId, tenantId, role } = ctx.session.user;

// In Procedures:
if (role !== 'admin' && role !== 'buero') {
throw new TRPCError({ code: 'FORBIDDEN' });
}

Client-seitige Implementierung​

// Conditional Rendering
const { hasPermission } = usePermissions();
{hasPermission('b2b_orders', 'write') && <CreateOrderButton />}

// Component Guard
<PermissionGate resource="invoices" action="write" fallback={<ReadOnlyView />}>
<InvoiceEditor />
</PermissionGate>

Regeln​

  • Rollen sind code-basiert (nicht pro Tenant konfigurierbar)
  • Permission-Check: per Request (Rolle aus DB, nicht JWT-gecached)
  • Bei 403: Fallback auf Readonly-Ansicht (nicht leere Seite)
  • Last-Admin-Schutz: Letzter Admin kann nicht herabgestuft werden
  • Neue User bekommen per Default Readonly (Security by Default)

Team-Einladungen​

Admin erstellt Einladung → E-Mail mit Token → User registriert sich → Automatische Zuordnung
  • Token: UUID, 7 Tage gültig
  • Einladung legt die Rolle fest
  • Bei Registrierung: Auth.js createUser-Event prüft ob Einladung existiert → Join statt neuen Tenant erstellen