10 KiB
10 KiB
Mitarbeiter-2FA per E-Mail-Code + Wechsel Ticketsystem ⇄ Projekt-Admin
Stand: 2026-09-30 · Status: Design vom Nutzer freigegeben (Teil 1 + Teil 2), Spec wartet auf Review
Ziel
- Jeder Mitarbeiter-Login (Ticketsystem ticket.webklar.com und Projekt-Admin project.webklar.com) verlangt nach dem Passwort einen 6-stelligen Code, der per E-Mail kommt. Pflicht für alle Mitarbeiter. Kunden bleiben unverändert.
- Aus der Seitenleiste des Ticketsystems springt man ohne zweiten Login ins Projekt-Admin; aus dem Projekt-Admin führt ein Button zurück ins Ticketsystem.
Entscheidungen des Nutzers: E-Mail-Code statt TOTP/KeePass; 2FA über Appwrite selbst (Variante A), nicht als reine Oberflächen-Sperre; Pflicht für alle Mitarbeiter.
Ausgangslage (geprüft)
- Beide Systeme nutzen dieselben Appwrite-Konten (Appwrite 1.8.1, Projekt
6a1058610003c5a13a05). - Appwrite verschickt bereits Mails:
_APP_SMTP_HOST=mail.your-server.de:587, AbsenderWEBklar <noreply@webklar.com>. - Appwrite-MFA (1.8.1): Ist
user.mfa = trueund die E-Mail bestätigt (emailVerification = true), braucht jede Sitzung 2 Faktoren. Eine Passwort-Sitzung hat nur den Faktorpassword; jeder Nicht-MFA-Aufruf liefert dann401 user_more_factors_required. Das gilt auch für bereits bestehende Sitzungen und für JWTs aus solchen Sitzungen.POST /v1/account/mfa/challenges{factor:'email'}→{ $id, expire }, schickt die Mail (VorlagemfaChallenge). Code gilt 1 Stunde. Limit 10 Anfragen pro Nutzer und Zeitfenster.PUT /v1/account/mfa/challenges{challengeId, otp}→ Sitzung bekommt Faktoremail. Falscher/abgelaufener Code:401 user_invalid_token.- Server-API mit Key:
PATCH /v1/users/:id/mfa {mfa},PATCH /v1/users/:id/verification {emailVerification}.
- Ticketsystem: Appwrite-Web-SDK 13.0.2 (kennt kein MFA). AuthContext meldet per
createEmailPasswordSessionan und prüft mitaccount.get(). - Projekt-Admin:
POST /api/auth/loginruft serverseitig/account/sessions/emailohne API-Key auf, nimmt nur dieuserIdund setzt die Portal-Sitzung (role: 'admin') für Mitarbeiter (resolveStaffAccount). Mit MFA wäre das eine Umgehung, weil die halbe Sitzung reicht. requireAppwriteStaff(JWT-Prüfung für/api/admin/*aus dem Ticketsystem) verlangt das Labeladmin. Mitarbeiter ohne Admin-Label kommen per Portal-Login trotzdem ins Projekt-Admin (staff.isAdmin = false).- Einmal-Link-Mechanismus existiert:
services/staffPreviewTokens.js(Map im Speicher, 60 s, einmalig), genutzt von/api/admin/preview-open→/api/auth/staff-preview. - Projekt-Admin (
public/admin.html) hat keine Seitenleiste, sondern Kopfzeile + Tabs.
Teil 1: Anmeldung mit E-Mail-Code
Ablauf für Mitarbeiter
- E-Mail + Passwort.
- Anzeige „Wir haben dir einen Code an k…@webklar.com geschickt“ + 6-stelliges Codefeld
(
inputmode="numeric",autocomplete="one-time-code"), Link „Code erneut senden“, Link „Abbrechen“ (meldet die halbe Sitzung ab, zurück zur Passwort-Eingabe). - Richtiger Code → angemeldet. Falscher/abgelaufener Code → „Code falsch oder abgelaufen“, Feld bleibt.
Ticketsystem (Repo JUSN/tickte-system)
- Kein SDK-Upgrade. Neues Modul
src/lib/mfa.jsmit zwei Funktionen überclient.call(...)des vorhandenen SDK-Clients (nutzt dieselbe Sitzung/Cookie-Fallback wie das SDK):createEmailChallenge()→challengeId;completeChallenge(challengeId, otp). Beide sendenX-Appwrite-Locale: de, damit die deutsche Vorlage greift. AuthContext:- neuer Zustand
mfaPending({ challengeId, email }odernull). login(): nach der Passwort-Sitzungaccount.get(); beiuser_more_factors_required→ Challenge anlegen,mfaPendingsetzen,{ success: false, mfaRequired: true }zurück.checkUser()beim Laden: gleicher Fall (bestehende halbe Sitzung, z. B. direkt nach dem Einschalten) → keine neue Mail automatisch, sondern Code-Schritt mit Knopf „Code senden“. Grund: jedes Neuladen würde sonst eine Mail auslösen und das Limit (10) aufbrauchen.verifyCode(otp),resendCode(),cancelMfa()(=deleteSession('current')).- Nach erfolgreichem Code wie bisher
account.get()+ensureEmployeeExists.
- neuer Zustand
LoginPage.jsx: zweiter Schritt „Code eingeben“; Routing: solangemfaPending, führt jede geschützte Route auf/login.- Fehlertexte:
user_invalid_token→ „Code falsch oder abgelaufen“;429/general_rate_limit_exceeded→ „Zu viele Versuche, bitte einige Minuten warten“;user_email_not_verified→ „2FA ist für dein Konto noch nicht eingerichtet – bitte Kenso Bescheid geben“.
Projekt-Admin (Repo Kundenportal, project.webklar.com)
loginWithAppwriteliest zusätzlich die Sitzung aus der Antwort (set-cookiea_session_<projectId>viaheaders.getSetCookie()), damit der Server im Namen dieser Sitzung weiterarbeiten kann.POST /api/auth/loginfür Mitarbeiter: Istaccount.mfa === true(ausgetUserById), wird keine Portal-Sitzung gesetzt. Stattdessen:- Challenge über die Sitzung anlegen, Eintrag in einer Map im Speicher
{ userId, email, appwriteSessionCookie, challengeId, expiresAt: +10 min, attempts: 0 }unter zufälligem Schlüssel; Schlüssel als signiertes, httpOnly-Cookiewebklar_portal_mfa. - Antwort
{ mfaRequired: true, email: <maskiert> }.
- Challenge über die Sitzung anlegen, Eintrag in einer Map im Speicher
POST /api/auth/login/mfa{ otp }: prüft den Code bei Appwrite (PUT /account/mfa/challengesmit der gespeicherten Sitzung). Erfolg → exakt der bisherige Mitarbeiter-Zweig (Portal-Sitzung + Preview-Cookie), Map-Eintrag + Cookie löschen. Max. 5 Fehlversuche pro Eintrag, dann Eintrag weg → neu anmelden.POST /api/auth/login/mfa/resend: neue Challenge für denselben Eintrag.- Serverneustart verwirft offene Einträge → Nutzer meldet sich neu an (akzeptiert).
- Kunden-Zweig bleibt unverändert (Kunden haben
mfa = false). public/login.html+app.js: zweiter Schritt mit Codefeld, gleiche Texte wie im Ticketsystem.
Einmalige Einrichtung (Skript, nach dem Livegang beider Teile)
- Skript
scripts/mitarbeiter-2fa.mjsim Kundenportal-Repo (läuft im Container mit dem Server-Key):--status: listet alle Mitarbeiter (Collectionemployees→userId) mitemailVerificationundmfa.--enable [userId|alle]: setztemailVerification = true, dannmfa = true.--disable <userId>: Notfall-Schalter, wenn bei einer Person keine Mail ankommt.- Vor dem Einschalten wird die E-Mail-Adresse jedes Kontos angezeigt; nur Adressen, die tatsächlich Post empfangen, werden bestätigt.
- Wirkung: Alle bestehenden Mitarbeiter-Sitzungen fragen beim nächsten Aufruf nach dem Code.
- Keine Wiederherstellungscodes (Faktor ist das Postfach; Notfall =
--disable).
Code-Mail
- Appwrite-Vorlage
mfaChallenge, Sprachede, als eigene Vorlage im Projekt hinterlegt (Betreff „Dein Anmeldecode für WEBklar“, Inhalt im Stil der WEBklar-Mailvorlagen: Logo, Code groß, „gilt 1 Stunde“, „Nicht du? Dann ändere bitte dein Passwort.“). - Wird über die Appwrite-Console gesetzt (Auth → Templates) – der Nutzer ist dort eingeloggt; ich bereite HTML und Betreff vor.
Teil 2: Wechsel zwischen Ticketsystem und Projekt-Admin
Hinweg (Ticketsystem → Projekt-Admin, ohne zweiten Login)
Navbar.jsx: neuer Eintrag „Projekt-Admin“ mit SymbolIconExternalLinknach „Finanzen“ / „Admin“, für alle Mitarbeiter sichtbar. Klick →openProjectAdmin()→window.location.assign(openUrl)(selber Tab). Während des Ladens deaktiviert; bei Fehler Fallback aufhttps://project.webklar.com/login.html.- Kundenportal:
POST /api/admin/switch-link(Bearer-JWT aus dem Ticketsystem):- eigene Prüfung statt
requireAppwriteStaff: JWT gültig (/account) undresolveStaffAccountfindet einen Mitarbeiter – so kommen auch Mitarbeiter ohne Admin-Label rüber, wie beim normalen Login. - Antwort
{ openUrl: 'https://project.webklar.com/api/auth/staff-switch?token=…' }, Token übercreateStaffPreviewToken(60 s, einmalig) mit{ userId, email, name, isAdmin, purpose: 'switch' }.
- eigene Prüfung statt
GET /api/auth/staff-switch?token=…:consumeStaffPreviewToken; nurpurpose === 'switch'akzeptiert (undstaff-previewakzeptiert umgekehrt keine Switch-Tokens). Setzt Portal-Sitzung + Preview-Cookie wie der Mitarbeiter-Login, Redirect/admin.html. Ungültig/abgelaufen → Redirect/login.html?error=Link abgelaufen, bitte anmelden.- 2FA bleibt gewahrt: Das JWT stammt aus einer Sitzung, die nach Teil 1 nur mit Code voll gültig ist.
Rückweg (Projekt-Admin → Ticketsystem)
admin.htmlKopfzeile: Button „← Ticketsystem“ (Linkhttps://ticket.webklar.com/tickets) vor „Abmelden“. Nur in der Admin-Ansicht; im Modus „Als Kunde ansehen“ und bei Kunden nicht.- Kein automatischer Login in diese Richtung: Ist die Ticketsystem-Sitzung noch aktiv, ist man direkt drin, sonst Login mit Code.
Tests
Mit nachgebautem Appwrite (Mock nach 1.8.1-Verhalten, keine echten Daten):
- Ticketsystem: Login ohne MFA unverändert; Login mit MFA → Code-Schritt; richtiger Code;
falscher Code; abgelaufener Code; erneut senden; Abbrechen; halbe Sitzung beim Neuladen (keine
automatische Mail); 429-Text; Seitenleisten-Eintrag öffnet
openUrl, Fehler → Fallback. - Projekt-Admin: Kunden-Login unverändert; Mitarbeiter ohne MFA unverändert; Mitarbeiter mit MFA
bekommt keine Portal-Sitzung vor dem Code; richtiger/falscher Code; 5 Fehlversuche; resend;
switch-link ohne/mit ungültigem JWT (401), mit halber Sitzung (401), Kunde (403), Mitarbeiter ohne
Admin-Label (200); staff-switch einmalig, abgelaufen, falscher
purpose; Rückweg-Button nur für Mitarbeiter und nicht im „Als Kunde ansehen“-Modus. - Bestehende Testsuites beider Repos grün.
Livegang (Reihenfolge)
- Kundenportal live (Push nach
maindurch den Nutzer bzw. auf sein OK). - Ticketsystem live (Push nach
testdurch den Nutzer). - Code-Mail-Vorlage in der Appwrite-Console hinterlegen.
mitarbeiter-2fa.mjs --status, dann zuerst nur für Kenso--enable, einmal in beiden Systemen durchspielen, dann--enable alle.
Ohne Schritt 4 ändert sich für niemanden etwas (MFA aus → Login wie bisher); Teil 2 funktioniert schon nach Schritt 1+2.
Nicht enthalten
- TOTP/KeePass, SMS, Wiederherstellungscodes, 2FA für Kunden.
- Automatischer Login vom Projekt-Admin zurück ins Ticketsystem.
- SDK-Upgrade des Ticketsystems.