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_SECRETgesetzt
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 akzeptierenjwt: Rolle + tenantId aus DB ladensession: 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:
| Aktion | Limit | Fenster |
|---|---|---|
| Logo-Upload | 10 Requests | /Minute/IP |
| Shopify OAuth | 10 Requests | /Minute/User |
| Order-Form Auth-Code | 3 Requests | /Stunde/E-Mail |
| Order-Form Auth-Code | 10 Requests | /Stunde/IP |
| Shopify Sync | 3 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
| Ressource | Admin | Röster | Packer | Büro | Readonly |
|---|---|---|---|---|---|
| Dashboard | W | W | W | W | R |
| Planung | W | W | — | — | R |
| Röst-Log | W | W | — | — | R |
| Rohkaffee | W | W | — | — | R |
| Maschinen | W | W | — | — | R |
| Tonnen | W | W | R | — | R |
| Abpackung | W | — | W | — | R |
| Profile | W | R | R | R | R |
| B2B Kunden | W | — | — | W | R |
| Bestellungen | W | — | — | W | R |
| Rechnungen | W | — | — | W | R |
| Preise | W | — | — | W | R |
| Finanzen | W | — | — | W | R |
| Rückverfolgung | W | W | R | W | R |
| Einstellungen | W | — | — | — | — |
| Team & Billing | W | — | — | — | — |
| Shopify/DHL | W | — | — | — | — |
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