Darstellung
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 viabcryptjs. - JWT-Sessions, kurzlebig:
maxAge = 4h(Session + JWT). Token trägtrole,companyId,permissionsundroleVersion(für Invalidierung bei Rollenwechsel). - Rolle wird aus
users.rolegelesen; fehlt sie, wird sie ausberufsgruppegemappt (getRoleFromBerufsgruppe), Fallback3(Mitarbeiter). - Login-UI:
/auth/signin,/auth/reset,/auth/error. Imapp-Surface rewritet Root/auf den Login (siehe 00-architektur-ueberblick).
Registrierung & Verifizierung
| Endpoint | Funktion |
|---|---|
POST /api/auth/register | Betrieb + Admin-User anlegen (Zod-validiert: companyName, companySlug [a-z0-9-], Admin-Daten, AGB/Datenschutz-Zustimmung). Optional inviteToken. |
POST /api/auth/verify-email | E-Mail bestätigen (Token aus email_verification_tokens) |
POST /api/auth/resend-verification | Verifizierungs-Mail erneut senden |
POST /api/auth/forgot-password | Reset-Mail anstoßen |
POST /api/auth/reset-password | Passwort ü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_SECRETsigniert (base64url + HMAC).
Team-Einladungen
| Endpoint | Funktion |
|---|---|
POST /api/team/invite | Mitarbeiter einladen (Rolle begrenzt durch resolveTeamInviteRole) |
POST /api/team/invite/resend · /cancel | Einladung erneut senden / zurückziehen |
GET /api/invites/details | Einladungsdetails (vor Annahme) |
POST /api/invites/accept | Einladung annehmen |
POST /api/invites/set-password | Passwort 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
usersund über alle offenen Invites. Domain-Checks liefern sprechende Fehler (createTeamPhoneInvitein@work7/domain), Eingabe muss im internationalen Format erfolgen. - Validierung per Template:
POST /api/team/invitemitphones: [{ phone, name? }]sendet die Meta-Template-NachrichtWHATSAPP_TEMPLATE_TEAM_INVITE(Sprache:WHATSAPP_TEMPLATE_TEAM_INVITE_LANGUAGE, Defaultde; Body-Parameter1= 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 (nurphone_number, kein Passwort/E-Mail) + Membership an (activatePhoneInvite) und schickt die Willkommensnachricht. Abgelaufene Einladungen (7 Tage) melden sich mit Hinweis auf erneutes Senden;/resendund/cancelakzeptierenphonestattemail. - 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
cancelTeamInviteden vonprepareInvitedUserProfilevorangelegten Datensatz mit (sofern kein Passwort, Login oder Membership existiert). Ab der ersten Aktivierung gilt: - Deaktivieren statt Löschen: User werden nie gelöscht (Archiv).
deleteTeamMembersetztmemberships.status = 'inactive',users.is_active = falseund 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 mitreactivate_user_idan (Migration 107) und sendet das Team-Invite-Template; antwortet die Nummer, schaltetactivatePhoneInviteden 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/streamund lädt beidomain:changed-Events mitentity = 'team'neu (Einladung erstellt/angenommen, Mitglied deaktiviert/reaktiviert). Invite-Annahmen publizierenteam.invite_accepted(WhatsApp:activatePhoneInviteim 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-codesendet einen 6-stelligen Code (Meta-TemplateWHATSAPP_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-Providerwhatsapp-otpprü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 inlib/whatsapp/phone-codes.ts. - Nummern sind immer verifiziert:
users.phone_numberwird nie ungeprüft geschrieben. Profil-Änderungen laufen überPOST /api/user/phone/request-code(Format- und systemweite Eindeutigkeits-Prüfung, Code an die neue Nummer) undPOST /api/user/phone/confirm. Die PATCH-Routen (/api/user,/api/profile/[userId]) lehnen direkte Nummern-Änderungen mitphone_verification_requiredab (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 procompany_id):current_step,dataJSONB.GET/POST /api/onboarding/stateundPOST /api/onboarding/steptreiben 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):
| Rolle | Wert | Default-Permissions |
|---|---|---|
| Administrator | 1 | admin:all |
| Manager | 2 | analytics, calendar, team_boards, projects, users, settings (read+write) |
| Mitarbeiter | 3 | analytics:read, calendar:read, team_boards:read, projects:read |
src/lib/permissions.ts–DEFAULT_ROLE_PERMISSIONS,PermissionService(hasPermission,hasAnyPermission,getUserPermissions).admin:allschlägt jede Einzelprüfung.- Pro-User-Overrides: Tabelle
user_permissions(resource,maskals Bitmaske) erlaubt abweichende Rechte je Nutzer. API:GET /api/permissions/user,POST /api/permissions/set,POST /api/permissions/reset. src/lib/authz.ts–can(actor, resource, action, scope)für feingranulare Checks (profiles,members; Aktionen READ/WRITE/ADMIN), inkl. „eigener Datensatz"-Ausnahme.- Domain-Guards (
@work7/domain):requireRole,requireTenantScope,requireContextsetzen 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()insrc/lib/auth.tsenthält noch vieleconsole.log('[DEBUG] …')– vor Prod bereinigen.two_factor_enabled/account_lockedexistieren als Spalten inusers, sind aber nicht im Login-Flow aktiv (kein 2FA-Erzwingen).- Rollen-Mapping aus
berufsgruppeist Legacy-Fallback; primär zähltusers.role.