22 KiB
Mitarbeiter-2FA per E-Mail-Code + Wechsel Ticketsystem ⇄ Projekt-Admin – Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Mitarbeiter melden sich in Ticketsystem und Projekt-Admin nur mit Passwort + E-Mail-Code an (Appwrite-MFA), und ein Seitenleisten-Eintrag wechselt ohne zweiten Login ins Projekt-Admin, ein Kopfzeilen-Button zurück.
Architecture: Appwrite 1.8.1 erzwingt den zweiten Faktor (user.mfa + emailVerification). Das Ticketsystem (React, SDK 13) ruft die MFA-Endpunkte über client.call auf; das Kundenportal (Express) führt beim Mitarbeiter-Login die Challenge serverseitig mit der Appwrite-Sitzung aus der Login-Antwort durch. Der Wechsel nutzt die vorhandenen Einmal-Tokens aus staffPreviewTokens.js mit neuem purpose: 'switch'.
Tech Stack: Kundenportal: Node/Express, node --test, Appwrite-Nachbau test/support/appwriteMock.js. Ticketsystem: React 18 + Vite, Appwrite-Web-SDK 13.0.2, neu node --test für reine Module; UI-Prüfung per esbuild-Harness + Playwright im Scratchpad (Rezept: Memory „woms-ticketsystem“, MemoryMax beachten).
Spec: docs/superpowers/specs/2026-09-30-mitarbeiter-2fa-und-wechsel-design.md (dieser Worktree)
Repos / Worktrees:
- Ticketsystem:
/home/webklar/apps/ticket.webklar.com/dev-2fa, Branchfeature/mitarbeiter-2fa(vonorigin/test3b6a821).node_modulesist getrackt → nurgit add <dateien>, nie-A. Nie pushen (Webhook deployt jeden Branch). - Kundenportal:
/home/webklar/apps/kundenbereich.webklar.com/dev-2fa, Branchfeature/mitarbeiter-2favonorigin/main(d5c70a5), anlegen in Task 1 (git -C build worktree add -b feature/mitarbeiter-2fa ../dev-2fa origin/main,node_modulesals Symlink auf../build/node_modules). Nicht pushen.
Global Constraints
- Appwrite-Endpunkte (1.8.1):
POST /v1/account/mfa/challenges {factor:'email'}→{ $id, expire };PUT /v1/account/mfa/challenges {challengeId, otp}; Admin:PATCH /v1/users/:id/mfa {mfa},PATCH /v1/users/:id/verification {emailVerification}. - Fehlertypen:
user_more_factors_required(401, halbe Sitzung),user_invalid_token(401, Code falsch/abgelaufen),user_email_not_verified,general_rate_limit_exceeded/ Status 429. - Code gilt 1 Stunde (Appwrite). Offene Portal-Code-Anmeldung: 10 Minuten, max. 5 Fehlversuche.
- Einmal-Link: 60 s, einmalig,
purpose: 'switch'; Preview-Tokens ohnepurposegelten als'preview'. - MFA-Aufrufe senden
X-Appwrite-Locale: de. - Texte (wörtlich, in beiden Systemen gleich):
- Hinweis:
Wir haben dir einen Code an {maskierteEmail} geschickt. - Feld-Label:
Code aus der E-Mail; Buttons:Anmelden,Code erneut senden,Abbrechen, (halbe Sitzung beim Laden)Code senden user_invalid_token→Code falsch oder abgelaufen.- 429 →
Zu viele Versuche. Bitte warte einige Minuten. user_email_not_verified→2FA ist für dein Konto noch nicht eingerichtet – bitte Kenso Bescheid geben.- Portal nach 5 Fehlversuchen / abgelaufen →
Bitte melde dich erneut an.
- Hinweis:
- E-Mail-Maskierung: erstes Zeichen des lokalen Teils +
…@+ Domain (kenso@webklar.com→k…@webklar.com). - Seitenleiste: Label
Projekt-Admin, IconIconExternalLink; Kopfzeile Portal:← Ticketsystem→https://ticket.webklar.com/tickets. - Kunden-Login und Mitarbeiter mit
mfa = falseverhalten sich exakt wie heute.
Review Focus
- Portal-Umgehung: Mitarbeiter mit
mfa = truedarf nachPOST /api/auth/loginkeinwebklar_portal_session- und kein Preview-Cookie bekommen – Test in Task 3. - Reload mit halber Sitzung im Ticketsystem: darf keine Mail automatisch auslösen und nicht in einer Login-Schleife enden – Test in Task 6 (UI-Harness).
- Token-Verwechslung: Ein Switch-Token darf nicht über
/api/auth/staff-previeweinlösbar sein und umgekehrt – Test in Task 4. - Halbe Sitzung beim Wechsel: JWT aus halber Sitzung →
/account401 →switch-link401, kein Token – Test in Task 4. - Abbrechen/Neu-Login mit halber Sitzung im Ticketsystem:
deleteSessions()vor dem Neu-Login muss auch mit halber Sitzung klappen (in Appwrite zur MFA-Gruppe gehörend? – wenn nicht, wird der Fehler geschluckt wie heute und die lokale Sitzung trotzdem gelöscht) – Test in Task 5.
Teil A – Kundenportal (kundenbereich.webklar.com/dev-2fa)
Task 1: Appwrite-Sitzung aus dem Login + MFA-Aufrufe im Namen der Sitzung
Files:
- Modify:
server/services/appwriteClient.js(appwriteFetch,loginWithAppwrite) - Modify:
test/support/appwriteMock.js(Sessions + MFA-Nachbau) - Test:
test/staffMfa.test.js(neu, erster Teil)
Interfaces:
-
Produces:
loginWithAppwrite(email, password) → { $id, email, name: '', sessionCookie: string }(sessionCookie= Wert vona_session_<projectId>ausset-cookie, sonst'').appwriteSessionFetch(path, { method, body, sessionCookie }) → data– wieappwriteFetch, zusätzlich HeaderCookie: a_session_<projectId>=<sessionCookie>undX-Appwrite-Locale: de; Fehler mit.status,.typewieappwriteFetch.- Mock:
POST /v1/account/sessions/email(setztset-cookie a_session_<project>=sess-<userId>; Nutzer ausmock.addUser({ $id, email, password, mfa, emailVerification, labels })),GET /v1/users/:idliefertmfa,emailVerification,labels,email;GET /v1/accountmit Cookie → 401user_more_factors_requiredsolange mfa und Faktor fehlt;POST/PUT /v1/account/mfa/challenges(OTP abrufbar übermock.lastOtp(userId),mock.expireChallenges());PATCH /v1/users/:id/mfa,PATCH /v1/users/:id/verification; Zählermock.mailsSent(userId).
-
Step 1: Worktree anlegen (siehe Kopf),
npm testim Worktree → alle bestehenden Tests grün (Ausgangslage notieren). -
Step 2: Failing test in
test/staffMfa.test.js:loginWithAppwrite liefert sessionCookie→assert.equal(user.sessionCookie, 'sess-u_staff').appwriteSessionFetch schickt Cookie und Locale→ Challenge anlegen,mock.mailsSent('u_staff') === 1, Mock hat Headerx-appwrite-locale: degesehen.
-
Step 3:
npm test→ FAIL (sessionCookieundefined / Funktion fehlt). -
Step 4: Implementieren:
appwriteFetchgibt intern auch die Response-Header zurück (response.headers.getSetCookie()),loginWithAppwritenimmt das Cookiea_session_${config.appwrite.projectId};appwriteSessionFetchexportieren. Mock erweitern. -
Step 5:
npm test→ PASS (alle). -
Step 6: Commit
git add server/services/appwriteClient.js test/support/appwriteMock.js test/staffMfa.test.js && git commit -m "Portal-Login: Appwrite-Sitzung merken, MFA-Aufrufe im Namen der Sitzung"
Task 2: Offene Code-Anmeldungen (staffMfa.js)
Files:
- Create:
server/services/staffMfa.js - Test:
test/staffMfa.test.js(erweitern)
Interfaces:
-
Consumes:
appwriteSessionFetch(Task 1). -
Produces:
maskEmail(email: string) → stringstartStaffMfa({ userId, email, sessionCookie }) → Promise<{ key: string, maskedEmail: string }>– legt Challenge an, Eintrag{ userId, email, sessionCookie, challengeId, expiresAt: now+10min, attempts: 0 }in einerMap.verifyStaffMfa(key, otp) → Promise<{ ok: true, userId, email } | { ok: false, reason: 'invalid' | 'gone' }>–invalidbeiuser_invalid_token(attempts+1; beim 5. Fehlversuch Eintrag löschen → danachgone),gonebei unbekanntem/abgelaufenem Schlüssel; bei Erfolg Eintrag löschen. Andere Fehler (z. B. 429) werden geworfen.resendStaffMfa(key) → Promise<boolean>– neue Challenge,challengeIdersetzen;falsewenn Eintrag weg.MFA_COOKIE = 'webklar_portal_mfa'
-
Step 1: Failing tests:
maskEmail:'kenso@webklar.com' → 'k…@webklar.com','a@b.de' → 'a…@b.de','' → ''.richtiger Code: start →verifyStaffMfa(key, mock.lastOtp('u_staff'))→{ ok: true, userId: 'u_staff', email }; zweiter Aufruf mit gleichem Key →{ ok:false, reason:'gone' }.falscher Code 5x: vier Malreason:'invalid', fünfterinvalid, sechstergone.abgelaufen: Zeit perDate.now-Stub +11 min →gone.resend:mock.mailsSentsteigt auf 2, alter OTP ungültig (invalid), neuer gültig.
-
Step 2:
npm test→ FAIL (Modul fehlt). -
Step 3:
server/services/staffMfa.jsimplementieren (SchlüsselrandomBytes(32).toString('hex'), Aufräumen wiestaffPreviewTokens.purgeExpired). -
Step 4:
npm test→ PASS. -
Step 5: Commit
git add server/services/staffMfa.js test/staffMfa.test.js && git commit -m "Portal: offene Mitarbeiter-Code-Anmeldungen (staffMfa)"
Task 3: Login-Route mit Code-Schritt + Login-Seite
Files:
- Create:
server/services/staffAccount.js(verschiebtresolveStaffAccountausroutes/auth.jsunverändert, exportiert) - Modify:
server/routes/auth.js(Login, neue Routen, HelferstartStaffPortalSession) - Modify:
public/login.html,public/app.js(initLoginPage),public/login.css(nur falls nötig) - Test:
test/staffLogin.test.js(neu, mitstartTestApp({ '/api/auth': authRoutes }))
Interfaces:
-
Consumes: Task 1 + 2.
-
Produces:
resolveStaffAccount(appwriteUserId, email, account) → { isAdmin, name, email } | null(ausserver/services/staffAccount.js).- In
auth.jslokal:startStaffPortalSession(res, { userId, staff }) → Promise<void>(setzt Portal-Sitzung + Preview-Cookie exakt wie heute der Mitarbeiter-Zweig; wird in Task 4 vonstaff-switchgenutzt – deshalb exportieren als benannten Export). POST /api/auth/login: Mitarbeiter mitaccount.mfa === true→ Status 200{ mfaRequired: true, email: maskedEmail }+ signiertes httpOnly-Cookiewebklar_portal_mfa(10 min,sameSite: 'lax',securewie Portal-Cookie); keine Portal-Sitzung.POST /api/auth/login/mfa {otp}→ 200{ success:true, role:'admin', admin }| 401{ error:'Code falsch oder abgelaufen.' }| 410{ error:'Bitte melde dich erneut an.' }(+ MFA-Cookie löschen) | 429.POST /api/auth/login/mfa/resend→ 200{ success:true }| 410.
-
Step 1: Failing tests (
test/staffLogin.test.js):Kunde ohne MFA: unverändert→ 200, Portal-Cookie gesetzt, keinmfaRequired.Mitarbeiter ohne MFA: unverändert→ 200role:'admin', Portal-Cookie gesetzt.Mitarbeiter mit MFA: kein Portal-Cookie vor dem Code→ 200{ mfaRequired:true, email:'k…@webklar.com' };set-cookieenthältwebklar_portal_mfa, nichtwebklar_portal_sessionund nicht das Preview-Cookie.GET /api/auth/memit diesem Cookie-Stand →authenticated:false.richtiger Code→ 200role:'admin', Portal-Cookie gesetzt, MFA-Cookie gelöscht;/me→role:'admin'.falscher Code→ 401 Text wörtlich; nach 5 falschen → 410Bitte melde dich erneut an..ohne MFA-Cookie→ 410.resend→ 200,mock.mailsSent= 2.
-
Step 2:
npm test→ FAIL. -
Step 3: Server implementieren:
resolveStaffAccountverschieben; im Login nachresolveStaffAccountbeiaccount?.mfa === true→startStaffMfa, sonst unverändertstartStaffPortalSession.stafffür den späteren Erfolg wird beiverifyneu pergetUserById+resolveStaffAccountermittelt (nicht im Speicher halten). -
Step 4:
npm test→ PASS (alle, inkl. bestehender). -
Step 5: Login-Seite: bei
result.mfaRequiredPasswort-Formular ausblenden, Code-Schritt zeigen (Texte aus Global Constraints, Feldinputmode="numeric" autocomplete="one-time-code" maxlength="6"),Abbrechen= Seite neu laden (/login.html). Erfolg →adminLoginTarget(). -
Step 6: Sichtprüfung mit Playwright gegen den Test-Server aus Step 1 (Mock + App): Passwort → Code-Schritt sichtbar, falscher Code zeigt Text, richtiger Code landet auf
/admin.html. Screenshot Handy- und Desktop-Breite. -
Step 7: Commit
git add server/services/staffAccount.js server/routes/auth.js public/login.html public/app.js test/staffLogin.test.js && git commit -m "Portal-Login: E-Mail-Code für Mitarbeiter mit 2FA"(+public/login.css, falls geändert)
Task 4: Wechsel-Link + Rückweg-Button
Files:
- Modify:
server/services/staffPreviewTokens.js - Create:
server/routes/admin/switchLink.js, mount inserver/index.jsalsapp.use('/api/admin/switch-link', adminSwitchLinkRoutes)nebenpreview-open - Modify:
server/routes/auth.js(neue RouteGET /staff-switch,staff-previewprüftpurpose) - Modify:
public/admin.html(Button in.portal-header-actionsvor#logout-btn) - Test:
test/staffSwitch.test.js(neu)
Interfaces:
-
Consumes:
resolveStaffAccount(Task 3),startStaffPortalSession(Task 3). -
Produces:
createStaffPreviewToken(payload)unverändert;consumeStaffPreviewToken(token, purpose = 'preview')→null, wenn(entry.purpose || 'preview') !== purpose(Token wird dabei trotzdem verbraucht).POST /api/admin/switch-link(Bearer-JWT):/accountper JWT (wierequireAppwriteStaff, aber ohne Label-Pflicht), danngetUserById+resolveStaffAccount; kein Mitarbeiter → 403; → 200{ openUrl: 'https://<config.preview.baseHost>/api/auth/staff-switch?token=…' }.GET /api/auth/staff-switch?token=→ gültig:startStaffPortalSession+302 /admin.html; sonst302 /login.html?error=Link%20abgelaufen%2C%20bitte%20anmelden.- Button:
<a class="portal-btn-outline" href="https://ticket.webklar.com/tickets">← Ticketsystem</a>.
-
Step 1: Failing tests:
switch-link ohne JWT→ 401;ungültiges JWT→ 401;JWT aus halber Sitzung(Mock liefertuser_more_factors_required) → 401.Kunde(jwt-kunde, kein employee) → 403.Mitarbeiter ohne admin-Label(neues Mock-JWTjwt-staff, employee-Dokument vorhanden) → 200 mitopenUrl.staff-switch gültig→ 302/admin.html, Portal-Cookierole:'admin',isAdminpassend.staff-switch zweimal→ zweiter Aufruf 302 auf/login.html?error=….staff-switch nach 61 s→ 302 Login.Switch-Token über staff-preview→ 410;Preview-Token über staff-switch→ 302 Login.admin.html enthält Rückweg-Link→ statischer Test: Datei enthälthref="https://ticket.webklar.com/tickets"innerhalbportal-header-actions.
-
Step 2:
npm test→ FAIL. -
Step 3: Implementieren.
-
Step 4:
npm test→ PASS (alle). -
Step 5: Commit
git add server/services/staffPreviewTokens.js server/routes/admin/switchLink.js server/index.js server/routes/auth.js public/admin.html test/staffSwitch.test.js && git commit -m "Wechsel Ticketsystem → Projekt-Admin per Einmal-Link, Rückweg-Button"
Task 5 (Portal): Einrichtungs-Skript scripts/mitarbeiter-2fa.mjs
Files:
- Create:
scripts/mitarbeiter-2fa.mjs,server/services/staffMfaAdmin.js - Test:
test/staffMfaAdmin.test.js
Interfaces:
-
Produces (
staffMfaAdmin.js):listStaffMfaStatus() → Promise<Array<{ userId, name, email, emailVerification, mfa }>>(alleemployeesmituserId);enableStaffMfa(userId)(erst verification, dann mfa);disableStaffMfa(userId). -
Skript:
--status(Tabelle),--enable <userId>|alle(zeigt vorher die Adressen, fragtJa/Neinauf stdin;--yesüberspringt),--disable <userId>. Läuft perdocker exec -w /app webklar-kundenbereich-server node scripts/mitarbeiter-2fa.mjs …. -
Step 1: Failing tests:
listStaffMfaStatusgegen Mock (2 employees, 1 ohneuserId→ ignoriert);enableStaffMfaruftverificationvormfa(Reihenfolge im Mock protokolliert);disableStaffMfasetzt nurmfa:false. -
Step 2: FAIL → Step 3: implementieren → Step 4:
npm testPASS. -
Step 5: Commit
git add scripts/mitarbeiter-2fa.mjs server/services/staffMfaAdmin.js test/staffMfaAdmin.test.js && git commit -m "Skript: 2FA für Mitarbeiter anzeigen, einschalten, abschalten"
Teil B – Ticketsystem (ticket.webklar.com/dev-2fa)
Task 6: MFA-Modul + AuthContext + Login-Seite
Files:
- Create:
src/lib/mfa.js,tests/mfa.test.mjs - Modify:
src/lib/appwrite.js(export { client }),src/context/AuthContext.jsx,src/pages/LoginPage.jsx,package.json("test": "node --test tests/")
Interfaces:
-
Produces (
src/lib/mfa.js, ohne Import vonappwrite.js, damit Node es laden kann):createMfaApi(client) → { createEmailChallenge(): Promise<string /*challengeId*/>, completeChallenge(challengeId, otp): Promise<void> }– überclient.call(method, new URL(client.config.endpoint + '/account/mfa/challenges'), { 'content-type': 'application/json', 'X-Appwrite-Locale': 'de' }, params).isMoreFactorsError(err) → boolean(err?.type === 'user_more_factors_required')mfaErrorMessage(err) → string(Texte aus Global Constraints; Fallbackerr.message || 'Anmeldung fehlgeschlagen')maskEmail(email) → string(gleiche Regel wie Portal)
-
AuthContext-Wert zusätzlich:
mfaPending: null | { challengeId: string | null, email: string },verifyCode(otp) → {success, error?},sendCode() → {success, error?}(erstmals oder erneut),cancelMfa() → Promise<void>.login()liefert bei MFA{ success:false, mfaRequired:true }. -
Step 1: Failing tests (
tests/mfa.test.mjs, Fake-Client zeichnetcall-Argumente auf):createEmailChallenge→ Methodepost, Pfad endet auf/account/mfa/challenges, Params{ factor:'email' }, HeaderX-Appwrite-Locale: de, Rückgabe =$id.completeChallenge('c1','123456')→ Methodeput, Params{ challengeId:'c1', otp:'123456' }.isMoreFactorsErrortrue/false;mfaErrorMessagefüruser_invalid_token, Code 429,user_email_not_verified, sonstige.maskEmail('kenso@webklar.com') === 'k…@webklar.com'.
-
Step 2:
npm test→ FAIL. -
Step 3:
mfa.jsimplementieren →npm testPASS. -
Step 4: AuthContext:
login(): nachcreateSessionWithCleanup→account.get();isMoreFactorsError→sendCode()+mfaPendingsetzen →{ success:false, mfaRequired:true }.checkUser():isMoreFactorsError→mfaPending = { challengeId: null, email: '' }, keine Mail,userbleibtnull, lokale Sitzung nicht löschen (anders als bei anderen 401).verifyCode:completeChallenge→account.get()→setUser,ensureEmployeeExists,mfaPending = null.cancelMfa:account.deleteSession('current')(Fehler schlucken),clearAllLocalAppwriteSessions(),mfaPending = null.- E-Mail für den Hinweis: beim Login aus dem Formular; beim Reload unbekannt → Hinweis ohne Adresse
Wir schicken dir einen Code an deine E-Mail-Adresse.bisCode sendengedrückt ist.
-
Step 5: LoginPage: bei
mfaPendingCode-Schritt statt Passwort-Formular (Texte wörtlich;Code sendennur wennchallengeId === null, sonst Codefeld +Code erneut senden);/loginleitet wie bisher nur beiuserweiter. -
Step 6: UI-Harness (Scratchpad, esbuild + Playwright, Appwrite per
page.routenachgebaut, Rezept aus Memory): Checks- Login ohne MFA →
/tickets. - Login mit MFA → Code-Schritt, Hinweis
k…@webklar.com, 1 Challenge-POST. - falscher Code →
Code falsch oder abgelaufen.; richtiger Code →/tickets. Code erneut senden→ 2. Challenge-POST.- Reload mit halber Sitzung → Code-Schritt mit
Code senden, 0 Challenge-POSTs, keine Weiterleitungsschleife (URL bleibt/login, max. 1GET /account). Abbrechen→DELETE /account/sessions/current, Passwort-Formular sichtbar; danach neuer Login möglich, auch wenn der MockDELETE /account/sessionsmit 401 beantwortet.- 429 beim Challenge-POST → Text
Zu viele Versuche. Bitte warte einige Minuten.
- Login ohne MFA →
-
Step 7:
npx vite buildim Worktree (untersystemd-run --scope -p MemoryMax=700M -p MemorySwapMax=0) → Build ok. -
Step 8: Commit
git add src/lib/mfa.js tests/mfa.test.mjs src/lib/appwrite.js src/context/AuthContext.jsx src/pages/LoginPage.jsx package.json && git commit -m "Login mit E-Mail-Code (Appwrite-2FA)"
Task 7: Seitenleisten-Eintrag „Projekt-Admin“
Files:
- Modify:
src/components/Navbar.jsx,src/lib/employeeAdminApi.js(oder neue kleinesrc/lib/projectAdminSwitch.jsim gleichen Stil)
Interfaces:
-
Consumes:
POST /api/admin/switch-link(Task 4). -
Produces:
openProjectAdmin() → Promise<void>–account.createJWT(), POST,window.location.assign(openUrl); bei Fehlerwindow.location.assign('https://project.webklar.com/login.html'). -
Step 1: Eintrag als
<button className="side-link">(kein<Link>, externes Ziel) nach der Linkliste in derselbenside-group, IconIconExternalLink, LabelProjekt-Admin, während des Ladensdisabled; im eingeklappten Zustandtitlegesetzt wie die anderen. -
Step 2: UI-Harness-Checks: Eintrag für Mitarbeiter ohne Admin-Rolle sichtbar; Klick → genau 1 POST auf
https://project.webklar.com/api/admin/switch-linkmitAuthorization: Bearer …, danach Navigation aufopenUrl; Fehlerantwort 500 → Navigation auf…/login.html. -
Step 3:
npm test+vite buildok. -
Step 4: Commit
git add src/components/Navbar.jsx src/lib/employeeAdminApi.js && git commit -m "Seitenleiste: direkt ins Projekt-Admin wechseln"
Teil C – Livegang (nur mit OK des Nutzers)
Task 8: Code-Mail-Vorlage + Übergabe
Files:
-
Create:
docs/superpowers/specs/2026-09-30-mfa-mailvorlage.html(Ticketsystem-Worktree) – BetreffDein Anmeldecode für WEBklar, HTML im Stil der Portal-Vorlagen (dev-mailvorlagen/server/mail-templates), Platzhalter{{otp}}, TextDer Code gilt 1 Stunde.undNicht du? Dann ändere bitte dein Passwort. -
Step 1: Vorlage erstellen, lokal mit
{{otp}} = 123456rendern, Screenshot an den Nutzer. -
Step 2: Übergabe an den Nutzer mit genauer Reihenfolge:
- Portal:
git -C /home/webklar/apps/kundenbereich.webklar.com/dev-2fa push origin feature/mitarbeiter-2fa:main– danach Live-Check:POST /api/admin/switch-linkohne JWT → 401, Login-Seite lädt. - Ticketsystem:
git -C /home/webklar/apps/ticket.webklar.com/dev-2fa push origin feature/mitarbeiter-2fa:test– danach Live-Check Bundle enthältProjekt-Admin. - Nutzer hinterlegt Vorlage in der Appwrite-Console (Auth → Templates → MFA-Challenge, Sprache Deutsch).
--status, dann--enable <Kensos userId>, Nutzer probiert beide Logins + Wechsel, dann--enable alle.
- Portal:
-
Step 3: Commit der Vorlage (
git add docs/superpowers/specs/2026-09-30-mfa-mailvorlage.html).