fix(auth): ChangePassword unterstützt jetzt DB-User — nicht nur Setup-Admin

Vorher hat ChangePassword das Passwort ausschließlich gegen den
setup-store-Admin geprüft und dort gespeichert. User die via
User-Management angelegt wurden, bekamen immer 401.

Fix: DB-Lookup via Users.FindByEmail, Verify + SetPassword im DB-Store.
Setup-Store-Admin bleibt synchron wenn die E-Mail übereinstimmt.
Fallback auf setup-store bleibt erhalten für Legacy-Installs (pre-DB).

Bonus: %m-Formatverb-Fehler im Scheduler-Alert-Text behoben.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-28 13:52:48 +02:00
parent 74d3f25d21
commit 7c50e28f55
5 changed files with 42 additions and 13 deletions

View File

@@ -194,12 +194,49 @@ type changePasswordRequest struct {
// braucht das hier das current_password als Confirmation — verhindert
// dass eine kompromittierte Session den Account übernimmt ohne dass
// das alte Passwort bekannt ist.
//
// Lookup-Reihenfolge: 1) DB users-Tabelle (alle multi-user-Accounts),
// 2) setup-store Admin-Fallback (Legacy / pre-DB). Beim Setup-Admin
// werden beide Stores synchron gehalten.
func (h *AuthHandler) ChangePassword(c *gin.Context) {
var req changePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err)
return
}
tok := CurrentToken(c)
if tok == nil {
response.Unauthorized(c, nil)
return
}
// 1. DB-backed user (alle via User-Management erstellten Accounts).
if h.Users != nil {
u, hash, dbErr := h.Users.FindByEmail(c.Request.Context(), tok.Actor)
if dbErr == nil {
if !usersvc.VerifyPassword(hash, req.CurrentPassword) {
response.Unauthorized(c, errors.New("invalid_current_password"))
return
}
if err := h.Users.SetPassword(c.Request.Context(), u.ID, req.NewPassword); err != nil {
response.Internal(c, err)
return
}
// Setup-Store-Admin synchron halten, falls gleiche E-Mail.
if st, _ := h.Setup.Load(); st != nil && strings.EqualFold(st.AdminEmail, tok.Actor) {
_ = h.Setup.SetAdminPassword(req.NewPassword)
}
if h.Audit != nil {
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "auth.password.change",
tok.Actor, gin.H{"actor": actorOf(c)}, h.NodeID)
}
response.OK(c, gin.H{"ok": true})
return
}
}
// 2. Fallback: setup-store Admin (vor DB-Migration oder nicht migriert).
st, err := h.Setup.Load()
if err != nil {
response.Internal(c, err)
@@ -209,10 +246,6 @@ func (h *AuthHandler) ChangePassword(c *gin.Context) {
response.Err(c, http.StatusServiceUnavailable, errors.New("setup_required"))
return
}
// Authorisierte Session ist nicht automatisch der Admin (Phase 4
// admin_users-Tabelle könnte mehrere Rollen haben). v1: aktuell
// nur der eine Admin-User; trotzdem prüfen wir das current_password
// gegen die persistierte Hash.
if !st.VerifyAdminPassword(req.CurrentPassword) {
response.Unauthorized(c, errors.New("invalid_current_password"))
return
@@ -225,10 +258,6 @@ func (h *AuthHandler) ChangePassword(c *gin.Context) {
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "auth.password.change",
st.AdminEmail, gin.H{"actor": actorOf(c)}, h.NodeID)
}
// Neue Session ausstellen — alte Cookie zeigt auf ein Token das
// noch gültig ist; das ist OK für UX (kein erzwungener Logout),
// sicherheitsbewusster: clearSession + force re-login. Wir
// halten's hier ruhig.
response.OK(c, gin.H{"ok": true})
}