# 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": "", "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.