110 lines
3.6 KiB
Go
110 lines
3.6 KiB
Go
package authtoken
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/user"
|
|
)
|
|
|
|
// ResetTokenTTL: 1 Stunde ist der uebliche Kompromiss zwischen Nutzbarkeit
|
|
// und Angriffsflaeche fuer Passwort-Reset-Links.
|
|
const ResetTokenTTL = time.Hour
|
|
|
|
// Notifier verschickt das Reset-Token an den Benutzer. Core CFG-02
|
|
// (Benachrichtigungs-Dispatcher) ist noch nicht in diesem Branch verfuegbar
|
|
// (siehe SICHERHEITSKONZEPT.md / Core-Board-Konvention: Versand laeuft
|
|
// grundsaetzlich ueber CFG-02, kein eigener E-Mail-Versand je Modul) — bis
|
|
// zur Anbindung protokolliert LogNotifier den Vorgang nur, statt eine E-Mail
|
|
// zu verschicken. Produktionsreif ist erst der Austausch gegen einen
|
|
// CFG-02-Client, keine neue eigene Versandlogik hier.
|
|
type Notifier interface {
|
|
NotifyPasswordReset(ctx context.Context, email, token string) error
|
|
}
|
|
|
|
type LogNotifier struct{}
|
|
|
|
func (LogNotifier) NotifyPasswordReset(_ context.Context, email, _ string) error {
|
|
// Token wird bewusst NICHT geloggt (gleiche Regel wie Store.Create).
|
|
slog.Info("passwort-reset angefordert (kein CFG-02 in diesem branch, nur protokolliert)", "email_domain_hint", domainHint(email))
|
|
return nil
|
|
}
|
|
|
|
func domainHint(email string) string {
|
|
for i := len(email) - 1; i >= 0; i-- {
|
|
if email[i] == '@' {
|
|
return email[i:]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Handler stellt Passwort-Reset als HTTP-Endpunkte bereit (IAM-08).
|
|
type Handler struct {
|
|
tokens *Store
|
|
users *user.TenantUserStore
|
|
notifier Notifier
|
|
}
|
|
|
|
func NewHandler(tokens *Store, users *user.TenantUserStore, notifier Notifier) *Handler {
|
|
if notifier == nil {
|
|
notifier = LogNotifier{}
|
|
}
|
|
return &Handler{tokens: tokens, users: users, notifier: notifier}
|
|
}
|
|
|
|
type requestResetRequest struct {
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
// RequestReset liefert IMMER dieselbe Erfolgsmeldung, unabhaengig davon, ob
|
|
// die E-Mail-Adresse existiert (Akzeptanzkriterium 3 / Pruefung 1,
|
|
// User-Enumeration-Schutz — gleiches Prinzip wie auth.LoginService.Login).
|
|
func (h *Handler) RequestReset(w http.ResponseWriter, r *http.Request) {
|
|
var req requestResetRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "ungueltige Anfrage", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Absichtlich: Fehler (unbekannte E-Mail) fuehrt NICHT zu einer anderen
|
|
// HTTP-Antwort als der Erfolgsfall — nur der interne Pfad unterscheidet sich.
|
|
creds, err := h.users.GetByEmailForAuth(r.Context(), req.Email)
|
|
if err == nil {
|
|
token, tokenErr := h.tokens.Create(r.Context(), creds.User.ID, PurposeReset, ResetTokenTTL)
|
|
if tokenErr == nil {
|
|
_ = h.notifier.NotifyPasswordReset(r.Context(), req.Email, token)
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
|
"message": "Falls ein Konto mit dieser E-Mail-Adresse existiert, wurde eine Nachricht mit weiteren Schritten verschickt.",
|
|
})
|
|
}
|
|
|
|
type completeResetRequest struct {
|
|
Token string `json:"token"`
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
|
|
// CompleteReset schliesst den Reset ab (Akzeptanzkriterium 3: generische
|
|
// Fehlermeldung bei ungueltigem/abgelaufenem Token, siehe ErrInvalidToken).
|
|
func (h *Handler) CompleteReset(w http.ResponseWriter, r *http.Request) {
|
|
var req completeResetRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "ungueltige Anfrage", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := CompletePasswordReset(r.Context(), h.tokens, h.users, req.Token, req.NewPassword); err != nil {
|
|
http.Error(w, "Link ungültig oder abgelaufen.", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|