Skip to content

01 – Auth, Onboarding & RBAC

Reifegrad: 🟢 produktiv · Owner-Schicht: src/lib/auth.ts, src/lib/permissions.ts, src/lib/authz.ts, src/app/api/auth/*, src/app/api/invites/*, src/app/api/onboarding/*

Zweck

Registrierung von Betrieben (Mandanten), Login, E-Mail-Verifizierung, Passwort-Reset, Team-Einladungen und das rollenbasierte Zugriffsmodell (RBAC).

Authentifizierung

  • NextAuth 4 mit CredentialsProvider (src/lib/auth.ts). Login per E-Mail oder Telefonnummer (identifier) + Passwort. Passwort-Hash via bcryptjs.
  • JWT-Sessions, kurzlebig: maxAge = 4h (Session + JWT). Token trägt role, companyId, permissions und roleVersion (für Invalidierung bei Rollenwechsel).
  • Rolle wird aus users.role gelesen; fehlt sie, wird sie aus berufsgruppe gemappt (getRoleFromBerufsgruppe), Fallback 3 (Mitarbeiter).
  • Login-UI: /auth/signin, /auth/reset, /auth/error. Im app-Surface rewritet Root / auf den Login (siehe 00-architektur-ueberblick).

Registrierung & Verifizierung

EndpointFunktion
POST /api/auth/registerBetrieb + Admin-User anlegen (Zod-validiert: companyName, companySlug [a-z0-9-], Admin-Daten, AGB/Datenschutz-Zustimmung). Optional inviteToken.
POST /api/auth/verify-emailE-Mail bestätigen (Token aus email_verification_tokens)
POST /api/auth/resend-verificationVerifizierungs-Mail erneut senden
POST /api/auth/forgot-passwordReset-Mail anstoßen
POST /api/auth/reset-passwordPasswort über Token zurücksetzen
  • Passwort-Policy (src/lib/password-policy.ts, NIST-orientiert): min. 12 Zeichen, je 1× Groß-/Kleinbuchstabe, Zahl, Sonderzeichen. Wird per Zod-Schema auch im UI live geprüft.
  • Rate-Limiting (src/lib/rate-limit.ts) auf Register/Login/Reset.
  • Invite-Token werden mit INVITE_TOKEN_SECRET/NEXTAUTH_SECRET signiert (base64url + HMAC).

Team-Einladungen

EndpointFunktion
POST /api/team/inviteMitarbeiter einladen (Rolle begrenzt durch resolveTeamInviteRole)
POST /api/team/invite/resend · /cancelEinladung erneut senden / zurückziehen
GET /api/invites/detailsEinladungsdetails (vor Annahme)
POST /api/invites/acceptEinladung annehmen
POST /api/invites/set-passwordPasswort beim ersten Login setzen

Speicherung in invites (token_hash, expires_at, accepted_at, role), UNIQUE (company_id, email). Domain-Logik: createTeamInvite, cancelTeamInvite, resolveTeamInviteRole in @work7/domain.

WhatsApp-Einladungen (ohne E-Mail)

Für Mitarbeiter ohne E-Mail-Adresse (z. B. Monteure) kann alternativ per WhatsApp-Nummer eingeladen werden (Team-Seite, Umschalter „Per WhatsApp"). Migration 106_whatsapp_invites.sql: invites.email nullable, neue Spalten phone_number + invited_name.

  • Nummer systemweit eindeutig: Unique-Indizes auf der normalisierten Nummer (nur Ziffern) in users und über alle offenen Invites. Domain-Checks liefern sprechende Fehler (createTeamPhoneInvite in @work7/domain), Eingabe muss im internationalen Format erfolgen.
  • Validierung per Template: POST /api/team/invite mit phones: [{ phone, name? }] sendet die Meta-Template-Nachricht WHATSAPP_TEMPLATE_TEAM_INVITE (Sprache: WHATSAPP_TEMPLATE_TEAM_INVITE_LANGUAGE, Default de; Body-Parameter 1 = Firmenname). Ohne konfiguriertes Template werden keine Phone-Invites angelegt.
  • Aktivierung beim Erstkontakt: Antwortet die eingeladene Nummer (beliebige Nachricht), ist das der Besitznachweis — der Agent (handle-inbound.ts) legt erst dann User (nur phone_number, kein Passwort/E-Mail) + Membership an (activatePhoneInvite) und schickt die Willkommensnachricht. Abgelaufene Einladungen (7 Tage) melden sich mit Hinweis auf erneutes Senden; /resend und /cancel akzeptieren phone statt email.
  • Löschen nur für nie aktive User: Wer nie aktiv war, wird beim Abbrechen der Einladung vollständig gelöscht — WhatsApp-Invites haben vor der Aktivierung ohnehin keinen User-Datensatz; beim E-Mail-Invite entfernt cancelTeamInvite den von prepareInvitedUserProfile vorangelegten Datensatz mit (sofern kein Passwort, Login oder Membership existiert). Ab der ersten Aktivierung gilt:
  • Deaktivieren statt Löschen: User werden nie gelöscht (Archiv). deleteTeamMember setzt memberships.status = 'inactive', users.is_active = false und löst die Telefonnummer vom User (phone_number = NULL) — Login und Agent-Zugang enden, die Nummer ist systemweit wieder frei, Historie (Zeiteinträge etc.) bleibt erhalten.
  • Reaktivieren: reactivateTeamMember (POST /api/team/member/reactivate) macht das rückgängig. WhatsApp-only Nutzer brauchen dabei wieder eine Nummer; die ist geschützt: sie darf weder einem anderen User (egal welcher Tenant) gehören noch in einer offenen Einladung stecken. Aktiv wird der Nutzer erst nach WhatsApp-Bestätigung: Die Reaktivierung legt ein Invite mit reactivate_user_id an (Migration 107) und sendet das Team-Invite-Template; antwortet die Nummer, schaltet activatePhoneInvite den bestehenden User wieder frei (statt einen neuen anzulegen). E-Mail-Nutzer werden direkt bzw. beim erneuten Invite-Accept (set-password) reaktiviert.
  • Live-Updates: Die Team-Seite abonniert /api/notifications/stream und lädt bei domain:changed-Events mit entity = 'team' neu (Einladung erstellt/angenommen, Mitglied deaktiviert/reaktiviert). Invite-Annahmen publizieren team.invite_accepted (WhatsApp: activatePhoneInvite im Agent-Worker, E-Mail: set-password-Route) — Pending → Active springt ohne Reload um. Sortierung: Mitglieder zuerst, offene Einladungen darunter.

WhatsApp-Login & verifizierte Telefonnummern

  • Login per WhatsApp-Code: Das Login-Formular hat einen Umschalter „Passwort / WhatsApp-Code". POST /api/auth/whatsapp-code sendet einen 6-stelligen Code (Meta-Template WHATSAPP_TEMPLATE_LOGIN_CODE, Kategorie AUTHENTICATION, mit Copy-Code-Button); die Antwort ist bewusst generisch (kein Nummern-Enumerieren), Rate-Limits pro Nummer (3/15 Min) und IP (10/15 Min). Der NextAuth-Provider whatsapp-otp prüft den Code und baut dieselbe Session wie der Passwort-Login.
  • Codes: Tabelle phone_verification_codes (Migration 108) — SHA-256-Hash, 5 Min TTL, einmalig, max. 5 Prüfversuche; Logik in lib/whatsapp/phone-codes.ts.
  • Nummern sind immer verifiziert: users.phone_number wird nie ungeprüft geschrieben. Profil-Änderungen laufen über POST /api/user/phone/request-code (Format- und systemweite Eindeutigkeits-Prüfung, Code an die neue Nummer) und POST /api/user/phone/confirm. Die PATCH-Routen (/api/user, /api/profile/[userId]) lehnen direkte Nummern-Änderungen mit phone_verification_required ab (Entfernen ist erlaubt, wenn ein E-Mail-Login existiert). Zusammen mit Invite/Reaktivierung gilt: Jede gespeicherte Nummer hat ihren Besitz per WhatsApp nachgewiesen.

Onboarding

  • onboarding_states (1 Zeile pro company_id): current_step, data JSONB.
  • GET/POST /api/onboarding/state und POST /api/onboarding/step treiben den Wizard.
  • UI: /onboarding/[company]. Schritte sammeln Firmen-Stammdaten (Adresse, Logo, Farben, Rechnungsformate – siehe 02-mandanten-company-settings).

Rollen- & Berechtigungsmodell (RBAC)

3 numerische Rollen (kleiner = höher privilegiert):

RolleWertDefault-Permissions
Administrator1admin:all
Manager2analytics, calendar, team_boards, projects, users, settings (read+write)
Mitarbeiter3analytics:read, calendar:read, team_boards:read, projects:read
  • src/lib/permissions.tsDEFAULT_ROLE_PERMISSIONS, PermissionService (hasPermission, hasAnyPermission, getUserPermissions). admin:all schlägt jede Einzelprüfung.
  • Pro-User-Overrides: Tabelle user_permissions (resource, mask als Bitmaske) erlaubt abweichende Rechte je Nutzer. API: GET /api/permissions/user, POST /api/permissions/set, POST /api/permissions/reset.
  • src/lib/authz.tscan(actor, resource, action, scope) für feingranulare Checks (profiles, members; Aktionen READ/WRITE/ADMIN), inkl. „eigener Datensatz"-Ausnahme.
  • Domain-Guards (@work7/domain): requireRole, requireTenantScope, requireContext setzen Rollen/Tenancy serverseitig durch – einheitlich für API und Agent.
  • Middleware (src/middleware.ts): /admin/** nur Rolle 1; geschützte Bereiche (/dashboard, /projects, /team, /leave) erzwingen Login.

Admin-Bereich

/admin (nur Rolle 1) + APIs: GET /api/admin/companies, GET /api/admin/users, POST /api/admin/users/update-role, POST /api/admin/access-invite.

Profil & DSGVO

/api/profile/*: Stammdaten ([userId]), Passwortänderung (password), Benachrichtigungs- Präferenzen (notifications), Aktionen (actions) und DSGVO-Export/Löschung (gdpr). Avatare: GET /api/user/avatar/[userId]. Sprache: POST /api/user/language.

Audit

Tabelle audit_logs (Aktion, Resource, old_value/new_value JSONB, IP, request_id, success). AI-spezifisches Audit in ai_audit (siehe 12-ai-speech-realtime).

Bekannte Befunde / offene Punkte

  • authorize() in src/lib/auth.ts enthält noch viele console.log('[DEBUG] …') – vor Prod bereinigen.
  • two_factor_enabled/account_locked existieren als Spalten in users, sind aber nicht im Login-Flow aktiv (kein 2FA-Erzwingen).
  • Rollen-Mapping aus berufsgruppe ist Legacy-Fallback; primär zählt users.role.

Verwandte Notes

Work7 · Software für Handwerksbetriebe