Reset Passwort funktioniert
This commit is contained in:
@@ -10,6 +10,12 @@ func BaseRouterGrp(r *gin.Engine) *gin.RouterGroup {
|
||||
// Routes
|
||||
r.POST("/register", user.RegisterUser)
|
||||
r.POST("/login", user.LoginUser)
|
||||
|
||||
// Password Reset Routes (unprotected)
|
||||
r.POST("/password-reset/request", user.RequestPasswordReset)
|
||||
r.GET("/password-reset/validate/:token", user.ValidateResetToken)
|
||||
r.POST("/password-reset/reset", user.ResetPassword)
|
||||
|
||||
protected := r.Group("/api")
|
||||
protected.Use(user.AuthMiddleware())
|
||||
return protected
|
||||
|
||||
@@ -8,6 +8,7 @@ package user
|
||||
import (
|
||||
"bamort/logger"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -199,3 +200,195 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// generateResetHash generiert einen sicheren Hash für Password-Reset
|
||||
func generateResetHash() (string, error) {
|
||||
bytes := make([]byte, 32)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// sendResetEmail simuliert das Senden einer E-Mail (hier nur Logging)
|
||||
// In einer echten Implementierung würde hier ein E-Mail-Service verwendet
|
||||
func sendResetEmail(email, username, resetHash, frontendURL string) error {
|
||||
// Verwende die mitgegebene Frontend-URL oder fallback auf Standard
|
||||
baseURL := frontendURL
|
||||
if baseURL == "" {
|
||||
baseURL = "http://localhost:3000" // Fallback, sollte aber nicht verwendet werden
|
||||
}
|
||||
|
||||
resetLink := fmt.Sprintf("%s/reset-password?token=%s", baseURL, resetHash)
|
||||
|
||||
logger.Info("=== PASSWORD RESET EMAIL ===")
|
||||
logger.Info("An: %s", email)
|
||||
logger.Info("Betreff: Passwort zurücksetzen für %s", username)
|
||||
logger.Info("Nachricht:")
|
||||
logger.Info("Hallo %s,", username)
|
||||
logger.Info("")
|
||||
logger.Info("Sie haben eine Passwort-Zurücksetzung angefordert.")
|
||||
logger.Info("Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen:")
|
||||
logger.Info("")
|
||||
logger.Info("%s", resetLink)
|
||||
logger.Info("")
|
||||
logger.Info("Dieser Link ist 14 Tage gültig.")
|
||||
logger.Info("Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail.")
|
||||
logger.Info("")
|
||||
logger.Info("=== END EMAIL ===")
|
||||
|
||||
// TODO: Hier echte E-Mail-Integration hinzufügen
|
||||
// z.B. SendGrid, SMTP, etc.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestPasswordReset Handler für Passwort-Reset-Anfrage
|
||||
func RequestPasswordReset(c *gin.Context) {
|
||||
logger.Debug("Starte Passwort-Reset-Anfrage...")
|
||||
|
||||
var input struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
RedirectURL string `json:"redirect_url,omitempty"` // Optionale Frontend-URL
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
logger.Error("Fehler beim Parsen der Reset-Anfrage: %s", err.Error())
|
||||
respondWithError(c, http.StatusBadRequest, "Gültige E-Mail-Adresse erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
// Frontend-URL aus Request verwenden
|
||||
redirectURL := input.RedirectURL
|
||||
if redirectURL == "" {
|
||||
// Fallback, sollte aber nicht verwendet werden, da Frontend die URL mitgeben sollte
|
||||
redirectURL = "http://localhost:3000"
|
||||
}
|
||||
|
||||
logger.Debug("Reset-Anfrage für E-Mail: %s", input.Email)
|
||||
|
||||
var user User
|
||||
if err := user.FindByEmail(input.Email); err != nil {
|
||||
// Aus Sicherheitsgründen keine Information preisgeben, ob die E-Mail existiert
|
||||
logger.Warn("Reset-Anfrage für nicht existierende E-Mail: %s", input.Email)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Falls ein Account mit dieser E-Mail-Adresse existiert, wurde eine Reset-E-Mail gesendet.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Generiere Reset-Hash
|
||||
resetHash, err := generateResetHash()
|
||||
if err != nil {
|
||||
logger.Error("Fehler beim Generieren des Reset-Hashes: %s", err.Error())
|
||||
respondWithError(c, http.StatusInternalServerError, "Fehler beim Verarbeiten der Anfrage")
|
||||
return
|
||||
}
|
||||
|
||||
// Speichere Reset-Hash in der Datenbank
|
||||
if err := user.SetPasswordResetHash(resetHash); err != nil {
|
||||
logger.Error("Fehler beim Speichern des Reset-Hashes für Benutzer %s: %s", user.Username, err.Error())
|
||||
respondWithError(c, http.StatusInternalServerError, "Fehler beim Verarbeiten der Anfrage")
|
||||
return
|
||||
}
|
||||
|
||||
// Sende Reset-E-Mail
|
||||
if err := sendResetEmail(user.Email, user.Username, resetHash, redirectURL); err != nil {
|
||||
logger.Error("Fehler beim Senden der Reset-E-Mail für Benutzer %s: %s", user.Username, err.Error())
|
||||
respondWithError(c, http.StatusInternalServerError, "Fehler beim Senden der E-Mail")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Reset-E-Mail erfolgreich für Benutzer %s (%s) gesendet", user.Username, user.Email)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Falls ein Account mit dieser E-Mail-Adresse existiert, wurde eine Reset-E-Mail gesendet.",
|
||||
})
|
||||
}
|
||||
|
||||
// ResetPassword Handler für das Zurücksetzen des Passworts
|
||||
func ResetPassword(c *gin.Context) {
|
||||
logger.Debug("Starte Passwort-Reset...")
|
||||
|
||||
var input struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
logger.Error("Fehler beim Parsen der Reset-Daten: %s", err.Error())
|
||||
respondWithError(c, http.StatusBadRequest, "Token und neues Passwort (mind. 6 Zeichen) erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("Reset-Versuch mit Token: %s", input.Token[:10]+"...")
|
||||
|
||||
var user User
|
||||
if err := user.FindByResetHash(input.Token); err != nil {
|
||||
logger.Warn("Ungültiger oder abgelaufener Reset-Token verwendet")
|
||||
respondWithError(c, http.StatusBadRequest, "Ungültiger oder abgelaufener Reset-Link")
|
||||
return
|
||||
}
|
||||
|
||||
// Zusätzliche Validierung des Tokens
|
||||
if !user.IsResetHashValid(input.Token) {
|
||||
logger.Warn("Reset-Token-Validierung fehlgeschlagen für Benutzer: %s", user.Username)
|
||||
respondWithError(c, http.StatusBadRequest, "Ungültiger oder abgelaufener Reset-Link")
|
||||
return
|
||||
}
|
||||
|
||||
// Neues Passwort hashen (gleiche Methode wie bei der Registrierung)
|
||||
hashedPassword := md5.Sum([]byte(input.NewPassword))
|
||||
user.PasswordHash = hex.EncodeToString(hashedPassword[:])
|
||||
|
||||
// Reset-Hash entfernen
|
||||
if err := user.ClearPasswordResetHash(); err != nil {
|
||||
logger.Error("Fehler beim Entfernen des Reset-Hashes für Benutzer %s: %s", user.Username, err.Error())
|
||||
respondWithError(c, http.StatusInternalServerError, "Fehler beim Aktualisieren des Accounts")
|
||||
return
|
||||
}
|
||||
|
||||
// Passwort speichern
|
||||
if err := user.Save(); err != nil {
|
||||
logger.Error("Fehler beim Speichern des neuen Passworts für Benutzer %s: %s", user.Username, err.Error())
|
||||
respondWithError(c, http.StatusInternalServerError, "Fehler beim Aktualisieren des Passworts")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Passwort erfolgreich zurückgesetzt für Benutzer: %s", user.Username)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Passwort erfolgreich zurückgesetzt",
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateResetToken Handler zur Validierung eines Reset-Tokens
|
||||
func ValidateResetToken(c *gin.Context) {
|
||||
logger.Debug("Validiere Reset-Token...")
|
||||
|
||||
token := c.Param("token")
|
||||
//token := c.Query("token")
|
||||
if token == "" {
|
||||
respondWithError(c, http.StatusBadRequest, "Token erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
var user User
|
||||
if err := user.FindByResetHash(token); err != nil {
|
||||
logger.Debug("Reset-Token nicht gefunden oder abgelaufen")
|
||||
respondWithError(c, http.StatusBadRequest, "Ungültiger oder abgelaufener Reset-Link")
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsResetHashValid(token) {
|
||||
logger.Debug("Reset-Token-Validierung fehlgeschlagen")
|
||||
respondWithError(c, http.StatusBadRequest, "Ungültiger oder abgelaufener Reset-Link")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("Reset-Token gültig für Benutzer: %s", user.Username)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"valid": true,
|
||||
"username": user.Username,
|
||||
"expires": user.ResetPwHashExpires,
|
||||
})
|
||||
}
|
||||
|
||||
+43
-6
@@ -9,12 +9,14 @@ import (
|
||||
)
|
||||
|
||||
type User struct {
|
||||
UserID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"unique" json:"username"`
|
||||
PasswordHash string `json:"password"`
|
||||
Email string `gorm:"unique" json:"email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UserID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"unique" json:"username"`
|
||||
PasswordHash string `json:"password"`
|
||||
Email string `gorm:"unique" json:"email"`
|
||||
ResetPwHash *string `gorm:"index" json:"-"` // Hash für Password-Reset (wird nicht serialisiert)
|
||||
ResetPwHashExpires *time.Time `json:"-"` // Ablaufzeit für Password-Reset-Hash
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (object *User) Create() error {
|
||||
@@ -55,3 +57,38 @@ func (object *User) Save() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByEmail findet einen User anhand der E-Mail-Adresse
|
||||
func (object *User) FindByEmail(email string) error {
|
||||
err := database.DB.First(&object, "email = ?", email).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// FindByResetHash findet einen User anhand des Reset-Hashes
|
||||
func (object *User) FindByResetHash(resetHash string) error {
|
||||
err := database.DB.First(&object, "reset_pw_hash = ? AND reset_pw_hash_expires > ?", resetHash, time.Now()).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// SetPasswordResetHash setzt den Reset-Hash und die Ablaufzeit (14 Tage)
|
||||
func (object *User) SetPasswordResetHash(resetHash string) error {
|
||||
expiryTime := time.Now().Add(14 * 24 * time.Hour) // 14 Tage gültig
|
||||
object.ResetPwHash = &resetHash
|
||||
object.ResetPwHashExpires = &expiryTime
|
||||
return object.Save()
|
||||
}
|
||||
|
||||
// ClearPasswordResetHash entfernt den Reset-Hash
|
||||
func (object *User) ClearPasswordResetHash() error {
|
||||
object.ResetPwHash = nil
|
||||
object.ResetPwHashExpires = nil
|
||||
return object.Save()
|
||||
}
|
||||
|
||||
// IsResetHashValid prüft ob der Reset-Hash gültig und nicht abgelaufen ist
|
||||
func (object *User) IsResetHashValid(resetHash string) bool {
|
||||
if object.ResetPwHash == nil || object.ResetPwHashExpires == nil {
|
||||
return false
|
||||
}
|
||||
return *object.ResetPwHash == resetHash && time.Now().Before(*object.ResetPwHashExpires)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user