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
PlanungWWR
Röst-LogWWR
RohkaffeeWWR
MaschinenWWR
TonnenWWRR
AbpackungWWR
ProfileWRRRR
B2B KundenWWR
BestellungenWWR
RechnungenWWR
PreiseWWR
FinanzenWWR
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