- Löschfristen (Migration 0020): pro Mandant einstellbar statt fest im Regelwerk, da die DSGVO selbst keine festen Fristen nennt (Art. 5 Abs. 1 lit. e). Jede Firma wird mit risikogestaffelten Vorschlagswerten vorbelegt, loeschfrist_max_tage wird jetzt tatsächlich hart gegen Werkzeuge gefiltert (schließt rules/OPEN.md Punkt 4). - Neues internal/mail-Paket (SMTP-Versand + Test-Doppel) und darauf aufbauend Passwort-Zurücksetzen (Migration 0019) - bisher nur als Absicht in der Rollentabelle genannt, nie gebaut. - dpf_zertifiziert für alle 20 Katalogeinträge gegen das offizielle DPF-Register recherchiert und in der Produktions-DB aktualisiert (12 zertifiziert, 5 recherchiert-nicht-gefunden, 3 nicht anwendbar). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
104 lines
4.2 KiB
Go
104 lines
4.2 KiB
Go
// 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)
|
|
}
|
|
}
|