// Passwort-Zurücksetzen (Ebene 1, öffentlich) — nutzt internal/mail für // den Versand. Wie beim Login verrät keine Fehlermeldung, ob eine // E-Mail-Adresse überhaupt existiert (User-Enumeration-Schutz): die // Anfrage-Seite zeigt immer dieselbe Erfolgsmeldung, unabhängig davon, // ob tatsächlich eine Mail verschickt wurde. package web import ( "fmt" "log" "net/http" "github.com/netcell-it/deklarix/internal/auth" ) const passwortVergessenHinweis = "Falls diese E-Mail-Adresse bei uns registriert ist, wurde ein Link zum Zurücksetzen verschickt." func (s *Server) handlePasswortVergessenForm(w http.ResponseWriter, r *http.Request) { s.renderAuthPage(w, "passwort-vergessen", authPageData{Title: "Passwort vergessen"}) } func (s *Server) handlePasswortVergessen(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { s.renderAuthPage(w, "passwort-vergessen", authPageData{Title: "Passwort vergessen", Error: "ungültiges Formular"}) return } email := r.FormValue("email") user, err := s.store.GetUserByEmail(r.Context(), email) if err == nil && user.Active { token, tokenErr := auth.NewSessionToken() if tokenErr == nil { if _, createErr := s.store.CreatePasswordResetToken(r.Context(), user.ID, token); createErr == nil { scheme := "http" if r.TLS != nil { scheme = "https" } link := fmt.Sprintf("%s://%s/passwort-zuruecksetzen/%s", scheme, r.Host, token) body := "Hallo,\n\nüber diesen Link kannst du dein Deklarix-Passwort zurücksetzen (gültig 1 Stunde):\n" + link + "\n\nFalls du das nicht angefordert hast, ignoriere diese E-Mail." // Versandfehler wird bewusst nicht an den Nutzer durchgereicht // (keine Enumeration über unterschiedliches Verhalten bei // Versandfehlern), aber serverseitig geloggt statt // verschluckt — sonst bleibt ein dauerhaft falsch // konfigurierter SMTP-Server unbemerkt. if err := s.mailer.Send(user.Email, "Deklarix — Passwort zurücksetzen", body); err != nil { log.Printf("passwort-vergessen: mail versand fehlgeschlagen: %v", err) } } } } s.renderAuthPage(w, "passwort-vergessen", authPageData{Title: "Passwort vergessen", Message: passwortVergessenHinweis}) } type passwortZuruecksetzenData struct { Title string Token string Error string } func (s *Server) handlePasswortZuruecksetzenForm(w http.ResponseWriter, r *http.Request) { token := r.PathValue("token") if _, err := s.store.GetValidPasswordResetToken(r.Context(), token); err != nil { http.Error(w, "Link ist ungültig oder abgelaufen", http.StatusNotFound) return } if err := s.templates.ExecuteTemplate(w, "passwort-zuruecksetzen", passwortZuruecksetzenData{Title: "Neues Passwort setzen", Token: token}); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } func (s *Server) handlePasswortZuruecksetzen(w http.ResponseWriter, r *http.Request) { token := r.PathValue("token") resetToken, err := s.store.GetValidPasswordResetToken(r.Context(), token) if err != nil { http.Error(w, "Link ist ungültig oder abgelaufen", http.StatusNotFound) return } if err := r.ParseForm(); err != nil { s.renderPasswortZuruecksetzenError(w, token, "ungültiges Formular") return } passwordHash, err := auth.HashPassword(r.FormValue("password")) if err != nil { s.renderPasswortZuruecksetzenError(w, token, err.Error()) return } if err := s.store.SetUserPassword(r.Context(), resetToken.UserID, passwordHash); err != nil { s.renderPasswortZuruecksetzenError(w, token, "Passwort konnte nicht gesetzt werden") return } if err := s.store.MarkPasswordResetTokenUsed(r.Context(), resetToken.ID); err != nil { s.renderPasswortZuruecksetzenError(w, token, "Passwort wurde gesetzt, Token konnte aber nicht als verwendet markiert werden") return } http.Redirect(w, r, "/login?reset=1", http.StatusSeeOther) } func (s *Server) renderPasswortZuruecksetzenError(w http.ResponseWriter, token, msg string) { if err := s.templates.ExecuteTemplate(w, "passwort-zuruecksetzen", passwortZuruecksetzenData{Title: "Neues Passwort setzen", Token: token, Error: msg}); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } }