IAM-08: benutzerprofil-login-oberflaeche (login+2fa/passwort-reset/profil-backend-handler + web/account next.js-frontend auf shl-01)
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user