2024-12-28 16:30:48 +01:00
|
|
|
/*
|
|
|
|
|
User Handlers
|
|
|
|
|
|
|
|
|
|
Add handlers for user registration and login:
|
|
|
|
|
*/
|
|
|
|
|
package user
|
|
|
|
|
|
|
|
|
|
import (
|
2025-08-11 07:46:53 +02:00
|
|
|
"bamort/logger"
|
2026-02-17 23:12:58 +01:00
|
|
|
"bamort/mail"
|
2025-01-04 21:29:33 +01:00
|
|
|
"crypto/md5"
|
2025-08-13 08:28:47 +02:00
|
|
|
"crypto/rand"
|
2025-01-04 21:29:33 +01:00
|
|
|
"encoding/hex"
|
|
|
|
|
"fmt"
|
2024-12-28 16:30:48 +01:00
|
|
|
"net/http"
|
2025-01-04 21:29:33 +01:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2026-02-03 17:21:43 +01:00
|
|
|
"unicode/utf8"
|
2024-12-28 16:30:48 +01:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-24 07:39:43 +02:00
|
|
|
func respondWithError(c *gin.Context, status int, message string) {
|
|
|
|
|
c.JSON(status, gin.H{"error": message})
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-28 16:30:48 +01:00
|
|
|
func RegisterUser(c *gin.Context) {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Starte Benutzerregistrierung...")
|
|
|
|
|
|
2024-12-30 15:46:18 +01:00
|
|
|
var user User
|
2024-12-28 16:30:48 +01:00
|
|
|
if err := c.ShouldBindJSON(&user); err != nil {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Error("Fehler beim Parsen der Registrierungsdaten: %s", err.Error())
|
2025-07-24 07:39:43 +02:00
|
|
|
respondWithError(c, http.StatusBadRequest, err.Error())
|
2024-12-28 16:30:48 +01:00
|
|
|
return
|
|
|
|
|
}
|
2025-08-11 07:46:53 +02:00
|
|
|
|
2025-08-30 08:59:45 +02:00
|
|
|
// Validate that username is not empty
|
|
|
|
|
if user.Username == "" {
|
|
|
|
|
logger.Error("Registrierung fehlgeschlagen - Benutzername ist leer")
|
|
|
|
|
respondWithError(c, http.StatusBadRequest, "Username cannot be empty")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate that email is not empty
|
|
|
|
|
if user.Email == "" {
|
|
|
|
|
logger.Error("Registrierung fehlgeschlagen - E-Mail ist leer")
|
|
|
|
|
respondWithError(c, http.StatusBadRequest, "Email cannot be empty")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate that password is not empty
|
|
|
|
|
if user.PasswordHash == "" {
|
|
|
|
|
logger.Error("Registrierung fehlgeschlagen - Passwort ist leer")
|
|
|
|
|
respondWithError(c, http.StatusBadRequest, "Password cannot be empty")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Registriere Benutzer: %s", user.Username)
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Printf("User input: '%s'", user.PasswordHash)
|
|
|
|
|
//hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.PasswordHash), bcrypt.DefaultCost)
|
|
|
|
|
hashedPassword := md5.Sum([]byte(user.PasswordHash))
|
|
|
|
|
user.PasswordHash = hex.EncodeToString(hashedPassword[:])
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Passwort-Hash erstellt für Benutzer: %s", user.Username)
|
|
|
|
|
|
2025-12-30 08:00:04 +01:00
|
|
|
// Set default role for new users
|
|
|
|
|
if user.Role == "" {
|
|
|
|
|
user.Role = RoleStandardUser
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Printf("pwdh: %s", user.PasswordHash)
|
|
|
|
|
if err := user.Create(); err != nil {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Error("Fehler beim Erstellen des Benutzers %s: %s", user.Username, err.Error())
|
2025-07-24 07:39:43 +02:00
|
|
|
respondWithError(c, http.StatusInternalServerError, fmt.Sprintf("Failed to create user: %s", err))
|
2024-12-28 16:30:48 +01:00
|
|
|
return
|
|
|
|
|
}
|
2025-08-11 07:46:53 +02:00
|
|
|
|
|
|
|
|
logger.Info("Benutzer erfolgreich registriert: %s (ID: %d)", user.Username, user.UserID)
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Printf(" ___ pwdh2: %s", user.PasswordHash)
|
2024-12-28 16:30:48 +01:00
|
|
|
c.JSON(http.StatusCreated, gin.H{"message": "User registered successfully:"})
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-04 21:29:33 +01:00
|
|
|
func GenerateToken(u *User) string {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Generiere Token für Benutzer: %s (ID: %d)", u.Username, u.UserID)
|
|
|
|
|
|
2025-01-04 21:29:33 +01:00
|
|
|
//u.Username + "lkiuztrew" + u.CreatedAt.String()
|
|
|
|
|
tx := md5.Sum([]byte(u.Username + u.CreatedAt.String()))
|
|
|
|
|
// Convert hash to raw string
|
|
|
|
|
hashString := hex.EncodeToString(tx[:])
|
|
|
|
|
pos := 7
|
|
|
|
|
idm := "." + fmt.Sprintf("%d", u.UserID) + ":"
|
|
|
|
|
// Insert the character
|
|
|
|
|
token := hashString[:pos] + string(idm) + hashString[pos:]
|
2025-08-11 07:46:53 +02:00
|
|
|
|
|
|
|
|
logger.Debug("Token erfolgreich generiert für Benutzer: %s", u.Username)
|
2025-01-04 21:29:33 +01:00
|
|
|
return token
|
|
|
|
|
}
|
|
|
|
|
func CheckToken(token string) *User {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Prüfe Token-Gültigkeit...")
|
|
|
|
|
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Print("CheckToken1: " + token)
|
|
|
|
|
var u User
|
|
|
|
|
var err error
|
|
|
|
|
pos := 7 + len("Bearer ")
|
|
|
|
|
userid := 0
|
|
|
|
|
// Check if a `.` is at position 7 (zero-indexed)
|
|
|
|
|
if len(token) > pos && token[pos] == '.' {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Token-Format erkannt, extrahiere Benutzer-ID...")
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Print("CheckToken2: " + token + "\n")
|
|
|
|
|
// Find the next `:` after the `.`
|
|
|
|
|
colonPos := strings.Index(token[pos+1:], ":") // Start searching after position 7
|
|
|
|
|
if colonPos != -1 {
|
|
|
|
|
//fmt.Printf("CheckToken3: %v\n", colonPos)
|
|
|
|
|
// Extract the substring between `.` and `:`
|
|
|
|
|
uu := token[pos+1 : pos+1+colonPos]
|
|
|
|
|
//fmt.Println("Extracted Substring:" + uu + "\n")
|
|
|
|
|
userid, err = strconv.Atoi(uu)
|
|
|
|
|
//fmt.Printf("Extracted UserID: %v \n", userid)
|
|
|
|
|
if err != nil {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Error("Fehler beim Parsen der Benutzer-ID aus Token: %s", err.Error())
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Print("CheckToken4: " + err.Error() + "\n")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Benutzer-ID aus Token extrahiert: %d", userid)
|
2025-01-04 21:29:33 +01:00
|
|
|
} else {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Token-Format ungültig: Kein ':' nach '.' gefunden")
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Print("CheckToken5: not found\n")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Token-Format ungültig: Kein '.' an erwarteter Position")
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Print("CheckToken6: not found\n")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if userid > 0 {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Lade Benutzer mit ID: %d", userid)
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Printf("CheckToken6-1: userid %v\n", userid)
|
|
|
|
|
//fmt.Printf("CheckToken6-1: userid %v\n", uint(userid))
|
|
|
|
|
err := u.FirstId(uint(userid))
|
|
|
|
|
if err != nil {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Error("Benutzer mit ID %d nicht gefunden: %s", userid, err.Error())
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Printf("CheckToken7: not found error %s\n", err.Error())
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Benutzer gefunden und Token validiert: %s (ID: %d)", u.Username, u.UserID)
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Printf("CheckToken8: found:%s \n", u.Username)
|
|
|
|
|
return &u
|
|
|
|
|
}
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Token-Validierung fehlgeschlagen: Ungültige Benutzer-ID")
|
2025-01-04 21:29:33 +01:00
|
|
|
//fmt.Print("CheckToken9: not found\n")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-28 16:30:48 +01:00
|
|
|
func LoginUser(c *gin.Context) {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Starte Benutzer-Anmeldung...")
|
|
|
|
|
|
2024-12-30 15:46:18 +01:00
|
|
|
var user User
|
2024-12-28 16:30:48 +01:00
|
|
|
var input struct {
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Password string `json:"password"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Error("Fehler beim Parsen der Login-Daten: %s", err.Error())
|
2025-07-24 07:39:43 +02:00
|
|
|
respondWithError(c, http.StatusBadRequest, err.Error())
|
2024-12-28 16:30:48 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Login-Versuch für Benutzer: %s", input.Username)
|
|
|
|
|
|
2025-01-04 21:29:33 +01:00
|
|
|
//if err := database.DB.Where("username = ?", input.Username).First(&user).Error; err != nil {
|
|
|
|
|
if err := user.First(input.Username); err != nil {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Warn("Login fehlgeschlagen - Benutzer nicht gefunden: %s", input.Username)
|
2025-07-24 07:39:43 +02:00
|
|
|
respondWithError(c, http.StatusUnauthorized, fmt.Sprintf("Invalid username. or password %v", input))
|
2024-12-28 16:30:48 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Benutzer gefunden, prüfe Passwort für: %s", input.Username)
|
2025-01-04 21:29:33 +01:00
|
|
|
hashedPassword := md5.Sum([]byte(input.Password))
|
|
|
|
|
fmt.Printf("pwdh: %s", hex.EncodeToString(hashedPassword[:]))
|
|
|
|
|
if user.PasswordHash != hex.EncodeToString(hashedPassword[:]) {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Warn("Login fehlgeschlagen - Ungültiges Passwort für Benutzer: %s", input.Username)
|
2025-07-24 07:39:43 +02:00
|
|
|
respondWithError(c, http.StatusUnauthorized, fmt.Sprintf("Invalid username. or password. %s %s", input.Password, hex.EncodeToString(hashedPassword[:])))
|
2024-12-28 16:30:48 +01:00
|
|
|
return
|
|
|
|
|
}
|
2025-01-04 21:29:33 +01:00
|
|
|
/*
|
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(input.Password)); err != nil {
|
2025-07-24 07:39:43 +02:00
|
|
|
respondWithError(c, http.StatusUnauthorized, "Invalid username or password.")
|
2025-01-04 21:29:33 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
*/
|
2024-12-28 16:30:48 +01:00
|
|
|
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Info("Login erfolgreich für Benutzer: %s (ID: %d)", user.Username, user.UserID)
|
|
|
|
|
token := GenerateToken(&user)
|
|
|
|
|
logger.Debug("Login-Token generiert für Benutzer: %s", user.Username)
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Login successful", "token": token})
|
2024-12-28 16:30:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply middleware to protected routes
|
|
|
|
|
func AuthMiddleware() gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Prüfe Authentifizierung für Request: %s %s", c.Request.Method, c.Request.URL.Path)
|
|
|
|
|
|
2024-12-28 16:30:48 +01:00
|
|
|
token := c.GetHeader("Authorization")
|
|
|
|
|
if token == "" {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Warn("Authentifizierung fehlgeschlagen - Kein Authorization-Header für %s %s", c.Request.Method, c.Request.URL.Path)
|
2025-07-24 07:39:43 +02:00
|
|
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
2024-12-28 16:30:48 +01:00
|
|
|
c.Abort()
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-08-10 22:26:07 +02:00
|
|
|
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Authorization-Header gefunden, prüfe Token...")
|
2025-08-10 22:26:07 +02:00
|
|
|
user := CheckToken(token)
|
|
|
|
|
if user == nil {
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Warn("Authentifizierung fehlgeschlagen - Ungültiger Token für %s %s", c.Request.Method, c.Request.URL.Path)
|
2025-07-24 07:39:43 +02:00
|
|
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized.")
|
2025-01-04 21:29:33 +01:00
|
|
|
c.Abort()
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-08-10 22:26:07 +02:00
|
|
|
|
2025-08-11 07:46:53 +02:00
|
|
|
logger.Debug("Authentifizierung erfolgreich für Benutzer: %s (ID: %d) - %s %s", user.Username, user.UserID, c.Request.Method, c.Request.URL.Path)
|
|
|
|
|
|
2025-08-10 22:26:07 +02:00
|
|
|
// Set user information in context
|
|
|
|
|
c.Set("userID", user.UserID)
|
|
|
|
|
c.Set("username", user.Username)
|
2025-12-30 08:00:04 +01:00
|
|
|
c.Set("user", user)
|
2024-12-28 16:30:48 +01:00
|
|
|
|
|
|
|
|
c.Next()
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-13 08:28:47 +02:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 23:12:58 +01:00
|
|
|
// sendResetEmail sends a password reset email via SMTP
|
2025-08-13 08:28:47 +02:00
|
|
|
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
|
|
|
|
|
}
|
2025-08-13 08:38:53 +02:00
|
|
|
|
2025-08-13 08:28:47 +02:00
|
|
|
resetLink := fmt.Sprintf("%s/reset-password?token=%s", baseURL, resetHash)
|
|
|
|
|
|
2026-02-17 23:12:58 +01:00
|
|
|
// Create mail client
|
|
|
|
|
mailClient := mail.NewClient()
|
|
|
|
|
|
|
|
|
|
// If mail is not configured, fallback to logging
|
|
|
|
|
if !mailClient.IsConfigured() {
|
|
|
|
|
logger.Warn("SMTP nicht konfiguriert - E-Mail wird nur geloggt")
|
|
|
|
|
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 ===")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build HTML email body
|
|
|
|
|
htmlBody := fmt.Sprintf(`<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<style>
|
|
|
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
|
|
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
|
|
|
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
|
|
|
|
|
.content { background-color: #f9f9f9; padding: 30px; margin-top: 20px; }
|
|
|
|
|
.button { display: inline-block; padding: 12px 30px; margin: 20px 0; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; }
|
|
|
|
|
.footer { margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="container">
|
|
|
|
|
<div class="header">
|
|
|
|
|
<h1>Passwort zurücksetzen</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="content">
|
|
|
|
|
<p>Hallo %s,</p>
|
|
|
|
|
<p>Sie haben eine Passwort-Zurücksetzung für Ihren Bamort-Account angefordert.</p>
|
|
|
|
|
<p>Klicken Sie auf den folgenden Button, um Ihr Passwort zurückzusetzen:</p>
|
|
|
|
|
<p style="text-align: center;">
|
|
|
|
|
<a href="%s" class="button">Passwort zurücksetzen</a>
|
|
|
|
|
</p>
|
|
|
|
|
<p>Oder kopieren Sie diesen Link in Ihren Browser:</p>
|
|
|
|
|
<p style="word-break: break-all; color: #666;">%s</p>
|
|
|
|
|
<p><strong>Dieser Link ist 14 Tage gültig.</strong></p>
|
|
|
|
|
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren. Ihr Passwort bleibt unverändert.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="footer">
|
|
|
|
|
<p>Dies ist eine automatisch generierte E-Mail. Bitte antworten Sie nicht auf diese Nachricht.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>`, username, resetLink, resetLink)
|
|
|
|
|
|
|
|
|
|
// Send email
|
|
|
|
|
msg := mail.Message{
|
|
|
|
|
To: email,
|
|
|
|
|
Subject: "Passwort zurücksetzen - Bamort",
|
|
|
|
|
Body: htmlBody,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := mailClient.Send(msg); err != nil {
|
|
|
|
|
logger.Error("Fehler beim Versenden der Reset-E-Mail an %s: %s", email, err.Error())
|
|
|
|
|
return err
|
|
|
|
|
}
|
2025-08-13 08:28:47 +02:00
|
|
|
|
2026-02-17 23:12:58 +01:00
|
|
|
logger.Info("Password-Reset-E-Mail erfolgreich an %s versendet", email)
|
2025-08-13 08:28:47 +02:00
|
|
|
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{
|
2026-02-03 17:21:43 +01:00
|
|
|
"valid": true,
|
|
|
|
|
"username": user.Username,
|
|
|
|
|
"display_name": user.DisplayNameOrUsername(),
|
|
|
|
|
"expires": user.ResetPwHashExpires,
|
2025-08-13 08:28:47 +02:00
|
|
|
})
|
|
|
|
|
}
|
2025-12-29 08:37:02 +01:00
|
|
|
|
|
|
|
|
// GetUserProfile Handler to get current user's profile information
|
|
|
|
|
func GetUserProfile(c *gin.Context) {
|
|
|
|
|
logger.Debug("Lade Benutzerprofil...")
|
|
|
|
|
|
|
|
|
|
// Get user ID from context (set by AuthMiddleware)
|
|
|
|
|
userID, exists := c.Get("userID")
|
|
|
|
|
if !exists {
|
|
|
|
|
logger.Error("Benutzer-ID nicht im Context gefunden")
|
|
|
|
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var user User
|
|
|
|
|
if err := user.FirstId(userID.(uint)); err != nil {
|
|
|
|
|
logger.Error("Benutzer mit ID %v nicht gefunden: %s", userID, err.Error())
|
|
|
|
|
respondWithError(c, http.StatusNotFound, "User not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Debug("Benutzerprofil geladen für: %s (ID: %d)", user.Username, user.UserID)
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
2026-01-14 15:35:51 +01:00
|
|
|
"id": user.UserID,
|
|
|
|
|
"username": user.Username,
|
2026-02-03 17:21:43 +01:00
|
|
|
"display_name": user.DisplayNameOrUsername(),
|
2026-01-14 15:35:51 +01:00
|
|
|
"email": user.Email,
|
|
|
|
|
"role": user.Role,
|
|
|
|
|
"preferred_language": user.PreferredLanguage,
|
2025-12-29 08:37:02 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UpdateEmail Handler to update user's email address
|
|
|
|
|
func UpdateEmail(c *gin.Context) {
|
|
|
|
|
logger.Debug("Starte E-Mail-Aktualisierung...")
|
|
|
|
|
|
|
|
|
|
// Get user ID from context
|
|
|
|
|
userID, exists := c.Get("userID")
|
|
|
|
|
if !exists {
|
|
|
|
|
logger.Error("Benutzer-ID nicht im Context gefunden")
|
|
|
|
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var input struct {
|
|
|
|
|
Email string `json:"email" binding:"required,email"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
|
|
|
logger.Error("Fehler beim Parsen der E-Mail-Daten: %s", err.Error())
|
|
|
|
|
respondWithError(c, http.StatusBadRequest, "Valid email address required")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var user User
|
|
|
|
|
if err := user.FirstId(userID.(uint)); err != nil {
|
|
|
|
|
logger.Error("Benutzer mit ID %v nicht gefunden: %s", userID, err.Error())
|
|
|
|
|
respondWithError(c, http.StatusNotFound, "User not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if email is already in use by another user
|
|
|
|
|
var existingUser User
|
|
|
|
|
if err := existingUser.FindByEmail(input.Email); err == nil {
|
|
|
|
|
if existingUser.UserID != user.UserID {
|
|
|
|
|
logger.Warn("E-Mail-Aktualisierung fehlgeschlagen - E-Mail bereits vergeben: %s", input.Email)
|
|
|
|
|
respondWithError(c, http.StatusConflict, "Email already in use")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user.Email = input.Email
|
|
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
|
logger.Error("Fehler beim Speichern der E-Mail für Benutzer %s: %s", user.Username, err.Error())
|
|
|
|
|
respondWithError(c, http.StatusInternalServerError, "Failed to update email")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Info("E-Mail erfolgreich aktualisiert für Benutzer: %s (ID: %d)", user.Username, user.UserID)
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
"message": "Email updated successfully",
|
|
|
|
|
"email": user.Email,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UpdatePassword Handler to update user's password
|
|
|
|
|
func UpdatePassword(c *gin.Context) {
|
|
|
|
|
logger.Debug("Starte Passwort-Aktualisierung...")
|
|
|
|
|
|
|
|
|
|
// Get user ID from context
|
|
|
|
|
userID, exists := c.Get("userID")
|
|
|
|
|
if !exists {
|
|
|
|
|
logger.Error("Benutzer-ID nicht im Context gefunden")
|
|
|
|
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var input struct {
|
|
|
|
|
CurrentPassword string `json:"current_password" binding:"required"`
|
|
|
|
|
NewPassword string `json:"new_password" binding:"required,min=6"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
|
|
|
logger.Error("Fehler beim Parsen der Passwort-Daten: %s", err.Error())
|
|
|
|
|
respondWithError(c, http.StatusBadRequest, "Current password and new password (min 6 characters) required")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var user User
|
|
|
|
|
if err := user.FirstId(userID.(uint)); err != nil {
|
|
|
|
|
logger.Error("Benutzer mit ID %v nicht gefunden: %s", userID, err.Error())
|
|
|
|
|
respondWithError(c, http.StatusNotFound, "User not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify current password
|
|
|
|
|
hashedCurrentPassword := md5.Sum([]byte(input.CurrentPassword))
|
|
|
|
|
if user.PasswordHash != hex.EncodeToString(hashedCurrentPassword[:]) {
|
|
|
|
|
logger.Warn("Passwort-Aktualisierung fehlgeschlagen - Aktuelles Passwort ungültig für Benutzer: %s", user.Username)
|
|
|
|
|
respondWithError(c, http.StatusUnauthorized, "Current password is incorrect")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hash new password
|
|
|
|
|
hashedNewPassword := md5.Sum([]byte(input.NewPassword))
|
|
|
|
|
user.PasswordHash = hex.EncodeToString(hashedNewPassword[:])
|
|
|
|
|
|
|
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
|
logger.Error("Fehler beim Speichern des Passworts für Benutzer %s: %s", user.Username, err.Error())
|
|
|
|
|
respondWithError(c, http.StatusInternalServerError, "Failed to update password")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Info("Passwort erfolgreich aktualisiert für Benutzer: %s (ID: %d)", user.Username, user.UserID)
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
"message": "Password updated successfully",
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-01-14 15:35:51 +01:00
|
|
|
|
|
|
|
|
// UpdateLanguage Handler to update user's preferred language
|
|
|
|
|
func UpdateLanguage(c *gin.Context) {
|
|
|
|
|
logger.Debug("Starte Sprach-Aktualisierung...")
|
|
|
|
|
|
|
|
|
|
// Get user ID from context
|
|
|
|
|
userID, exists := c.Get("userID")
|
|
|
|
|
if !exists {
|
|
|
|
|
logger.Error("Benutzer-ID nicht im Context gefunden")
|
|
|
|
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var input struct {
|
|
|
|
|
Language string `json:"language" binding:"required"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
|
|
|
logger.Error("Fehler beim Parsen der Sprach-Daten: %s", err.Error())
|
|
|
|
|
respondWithError(c, http.StatusBadRequest, "Language is required")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate language (only de and en supported)
|
|
|
|
|
if input.Language != "de" && input.Language != "en" {
|
|
|
|
|
logger.Warn("Ungültige Sprache angefordert: %s", input.Language)
|
|
|
|
|
respondWithError(c, http.StatusBadRequest, "Invalid language. Supported languages: de, en")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var user User
|
|
|
|
|
if err := user.FirstId(userID.(uint)); err != nil {
|
|
|
|
|
logger.Error("Benutzer mit ID %v nicht gefunden: %s", userID, err.Error())
|
|
|
|
|
respondWithError(c, http.StatusNotFound, "User not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user.PreferredLanguage = input.Language
|
|
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
|
logger.Error("Fehler beim Speichern der Sprache für Benutzer %s: %s", user.Username, err.Error())
|
|
|
|
|
respondWithError(c, http.StatusInternalServerError, "Failed to update language")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Info("Sprache erfolgreich aktualisiert für Benutzer: %s (ID: %d) - Neue Sprache: %s", user.Username, user.UserID, input.Language)
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
"message": "Language updated successfully",
|
|
|
|
|
"language": user.PreferredLanguage,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-02-03 17:21:43 +01:00
|
|
|
|
|
|
|
|
// UpdateDisplayName Handler to update user's display name
|
|
|
|
|
func UpdateDisplayName(c *gin.Context) {
|
|
|
|
|
logger.Debug("Starte Anzeigenamen-Aktualisierung...")
|
|
|
|
|
|
|
|
|
|
userID, exists := c.Get("userID")
|
|
|
|
|
if !exists {
|
|
|
|
|
logger.Error("Benutzer-ID nicht im Context gefunden")
|
|
|
|
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var input struct {
|
|
|
|
|
DisplayName string `json:"display_name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
|
|
|
logger.Error("Fehler beim Parsen des Anzeigenamens: %s", err.Error())
|
|
|
|
|
respondWithError(c, http.StatusBadRequest, "Display name is required")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if utf8.RuneCountInString(input.DisplayName) > 30 {
|
|
|
|
|
logger.Warn("Anzeigename zu lang: %d Zeichen", utf8.RuneCountInString(input.DisplayName))
|
|
|
|
|
respondWithError(c, http.StatusBadRequest, "Display name must be at most 30 characters")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var user User
|
|
|
|
|
if err := user.FirstId(userID.(uint)); err != nil {
|
|
|
|
|
logger.Error("Benutzer mit ID %v nicht gefunden: %s", userID, err.Error())
|
|
|
|
|
respondWithError(c, http.StatusNotFound, "User not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user.DisplayName = input.DisplayName
|
|
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
|
logger.Error("Fehler beim Speichern des Anzeigenamens für Benutzer %s: %s", user.Username, err.Error())
|
|
|
|
|
respondWithError(c, http.StatusInternalServerError, "Failed to update display name")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Info("Anzeigename erfolgreich aktualisiert für Benutzer: %s (ID: %d)", user.Username, user.UserID)
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
"message": "Display name updated successfully",
|
|
|
|
|
"display_name": user.DisplayNameOrUsername(),
|
|
|
|
|
})
|
|
|
|
|
}
|