auth.ValidatePassword checks every way a password is set: setup, registration, profile, admin create and reset, reset by mail. Same rule in the browser before sending (lib/password.ts), with our own German message instead of the browser popup. Existing shorter passwords still log in. Profile and admin reset no longer trim the password, which made passwords with surrounding spaces unusable for login. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.2 KiB
Go
92 lines
3.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/sandrodach/mindsift/internal/auth"
|
|
"github.com/sandrodach/mindsift/internal/storage"
|
|
)
|
|
|
|
// passwordResetAvailable: mails can be sent and links point to a known address (BASE_URL).
|
|
func (s *Server) passwordResetAvailable() bool {
|
|
if !s.cfg.BaseURLSet || s.cfg.DemoMode {
|
|
return false
|
|
}
|
|
_, err := s.smtpSettings()
|
|
return err == nil
|
|
}
|
|
|
|
// handleForgotPassword always answers the same, whether the address exists or not, and sends the mail
|
|
// in the background so the response time does not tell either.
|
|
func (s *Server) handleForgotPassword(w http.ResponseWriter, r *http.Request) {
|
|
if !s.passwordResetAvailable() {
|
|
writeError(w, http.StatusServiceUnavailable, "Passwort-Zurücksetzen ist auf diesem Server nicht eingerichtet. Bitte wende dich an die Administration.")
|
|
return
|
|
}
|
|
var req struct {
|
|
Email string `json:"email"`
|
|
}
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&req); err != nil || strings.TrimSpace(req.Email) == "" {
|
|
writeError(w, http.StatusBadRequest, "E-Mail-Adresse erforderlich")
|
|
return
|
|
}
|
|
email := strings.TrimSpace(req.Email)
|
|
go func() {
|
|
user, err := s.store.GetUserByEmail(email)
|
|
if err != nil || user == nil || !user.IsActive {
|
|
return
|
|
}
|
|
token, err := s.store.CreatePasswordResetToken(user.ID)
|
|
if err != nil {
|
|
if !errors.Is(err, storage.ErrResetTooSoon) {
|
|
slog.Warn("Passwort-Reset: Token konnte nicht erzeugt werden", "error", err)
|
|
}
|
|
return
|
|
}
|
|
link := strings.TrimRight(s.cfg.BaseURL, "/") + "/?resetToken=" + url.QueryEscape(token)
|
|
body := fmt.Sprintf("Hallo %s,\n\nfür dein MindSift-Konto wurde ein neues Passwort angefordert. "+
|
|
"Über diesen Link kannst du es innerhalb der nächsten Stunde festlegen:\n\n%s\n\n"+
|
|
"Der Link funktioniert nur einmal. Hast du das nicht angefordert, ignoriere diese E-Mail; dein Passwort bleibt unverändert.\n",
|
|
user.DisplayName, link)
|
|
_ = s.sendEmailNotification(user.Email, "MindSift: Passwort zurücksetzen", body)
|
|
}()
|
|
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleResetPassword(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Token string `json:"token"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&req); err != nil || req.Token == "" {
|
|
writeError(w, http.StatusBadRequest, storage.ErrResetTokenInvalid.Error())
|
|
return
|
|
}
|
|
if err := auth.ValidatePassword(req.Password); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
hash, err := auth.HashPassword(req.Password)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Passwort konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
userID, err := s.store.ResetPasswordWithToken(req.Token, hash)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrResetTokenInvalid) {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "Passwort konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
slog.Info("Passwort per E-Mail-Link zurückgesetzt", "user_id", userID)
|
|
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|