mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
630 lines
20 KiB
Go
630 lines
20 KiB
Go
package controllers
|
|
|
|
import (
|
|
cryptorand "crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"fotbal-club/internal/config"
|
|
"fotbal-club/internal/models"
|
|
"fotbal-club/pkg/email"
|
|
"fotbal-club/pkg/logger"
|
|
"fotbal-club/pkg/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
verificationCodeLength = 6
|
|
verificationCodeExpiry = 10 * time.Minute
|
|
maxVerificationAttempts = 3
|
|
)
|
|
|
|
type PasswordController struct {
|
|
DB *gorm.DB
|
|
EmailService email.EmailService
|
|
}
|
|
|
|
// generateVerificationCode generates a random 6-digit verification code (crypto-secure)
|
|
func generateVerificationCode() string {
|
|
const digits = "0123456789"
|
|
b := make([]byte, verificationCodeLength)
|
|
for i := 0; i < verificationCodeLength; i++ {
|
|
// draw a random byte and map into 0-9 range without modulo bias concerns for small range
|
|
var rb [1]byte
|
|
if _, err := cryptorand.Read(rb[:]); err != nil {
|
|
// fallback to time-based digit if crypto fails (extremely unlikely)
|
|
b[i] = digits[int(time.Now().UnixNano())%10]
|
|
continue
|
|
}
|
|
b[i] = digits[int(rb[0])%10]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// InitiatePasswordReset starts the password reset process by sending a verification code
|
|
func (pc *PasswordController) InitiatePasswordReset(c *gin.Context) {
|
|
var req InitiatePasswordResetRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Neplatný formát požadavku: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
emailAddr := strings.TrimSpace(strings.ToLower(req.Email))
|
|
if emailAddr == "" || !strings.Contains(emailAddr, "@") {
|
|
c.JSON(http.StatusBadRequest, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Neplatná e-mailová adresa",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Check if email service is available
|
|
if pc.EmailService == nil {
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "E-mailová služba není k dispozici. Kontaktujte prosím správce.",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Check if user exists
|
|
var user models.User
|
|
if err := pc.DB.Where("LOWER(email) = LOWER(?)", emailAddr).First(&user).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
// Return success to prevent email enumeration
|
|
c.JSON(http.StatusOK, PasswordResetResponse{
|
|
Success: true,
|
|
Message: "Pokud účet s tímto e-mailem existuje, byl odeslán ověřovací kód.",
|
|
NextStep: "verify_code",
|
|
})
|
|
return
|
|
}
|
|
// Other database error
|
|
logger.Error("Database error when checking user: error=%v email=%s", err, emailAddr)
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Došlo k chybě při ověřování e-mailu",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Generate a secure random token
|
|
token := make([]byte, 32)
|
|
if _, err := cryptorand.Read(token); err != nil {
|
|
logger.Error("Failed to generate random token: %v", err)
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Nepodařilo se vygenerovat bezpečnostní token",
|
|
})
|
|
return
|
|
}
|
|
tokenStr := hex.EncodeToString(token)
|
|
|
|
// Generate verification code
|
|
verificationCode := generateVerificationCode()
|
|
expiresAt := time.Now().Add(verificationCodeExpiry)
|
|
|
|
// Create or update password reset record
|
|
var pr models.PasswordReset
|
|
err := pc.DB.Transaction(func(tx *gorm.DB) error {
|
|
// Invalidate any existing reset tokens for this user
|
|
if err := tx.Model(&models.PasswordReset{}).
|
|
Where("user_id = ? AND used_at IS NULL", user.ID).
|
|
Update("used_at", time.Now()).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create new reset record
|
|
pr = models.PasswordReset{
|
|
UserID: user.ID,
|
|
Token: tokenStr,
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
VerificationCode: verificationCode,
|
|
VerificationCodeExpires: &expiresAt,
|
|
VerificationAttempts: 0,
|
|
IP: c.ClientIP(),
|
|
UserAgent: c.Request.UserAgent(),
|
|
}
|
|
|
|
return tx.Create(&pr).Error
|
|
})
|
|
|
|
if err != nil {
|
|
logger.Error("Failed to create password reset record: %v", err)
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Nepodařilo se vytvořit záznam pro obnovu hesla",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Send verification code via email
|
|
if seCode, ok := pc.EmailService.(interface{ SendPasswordResetCode(to, code string) error }); ok {
|
|
if err := seCode.SendPasswordResetCode(user.Email, verificationCode); err != nil {
|
|
// Fallback to legacy method if available
|
|
if seLegacy, ok2 := pc.EmailService.(interface {
|
|
SendPasswordReset(to, resetLink string, useOverride bool) error
|
|
}); ok2 {
|
|
if err2 := seLegacy.SendPasswordReset(user.Email, verificationCode, true); err2 != nil {
|
|
logger.Error("Failed to send verification code (both methods): error=%v email=%s", err2, emailAddr)
|
|
if !config.AppConfig.Debug {
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Nepodařilo se odeslat ověřovací kód",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
logger.Error("No available email method for sending verification code: email=%s error=%v", emailAddr, err)
|
|
if !config.AppConfig.Debug {
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Nepodařilo se odeslat ověřovací kód",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
} else if seLegacy, ok := pc.EmailService.(interface {
|
|
SendPasswordReset(to, resetLink string, useOverride bool) error
|
|
}); ok {
|
|
if err := seLegacy.SendPasswordReset(user.Email, verificationCode, true); err != nil {
|
|
logger.Error("Failed to send verification code (legacy): error=%v email=%s", err, emailAddr)
|
|
if !config.AppConfig.Debug {
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Nepodařilo se odeslat ověřovací kód",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, PasswordResetResponse{
|
|
Success: true,
|
|
Message: "Ověřovací kód byl odeslán na váš e-mail.",
|
|
NextStep: "verify_code",
|
|
CodeRequired: true,
|
|
})
|
|
}
|
|
|
|
// VerifyResetCode verifies the reset code and returns a reset token if valid
|
|
func (pc *PasswordController) VerifyResetCode(c *gin.Context) {
|
|
var req VerifyResetCodeRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Neplatný formát požadavku: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
emailAddr := strings.TrimSpace(strings.ToLower(req.Email))
|
|
if emailAddr == "" || !strings.Contains(emailAddr, "@") {
|
|
c.JSON(http.StatusBadRequest, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Neplatná e-mailová adresa",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Find the user
|
|
var user models.User
|
|
if err := pc.DB.Where("LOWER(email) = LOWER(?)", emailAddr).First(&user).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Uživatel s tímto e-mailem nebyl nalezen",
|
|
})
|
|
return
|
|
}
|
|
logger.Error("Database error when finding user: error=%v email=%s", err, emailAddr)
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Došlo k chybě při ověřování kódu",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Find the active reset record
|
|
var pr models.PasswordReset
|
|
now := time.Now()
|
|
err := pc.DB.Transaction(func(tx *gorm.DB) error {
|
|
// Find the most recent active reset record
|
|
if err := tx.Where("user_id = ? AND used_at IS NULL AND expires_at > ?", user.ID, now).
|
|
Order("created_at DESC").First(&pr).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if verification code has expired
|
|
if pr.VerificationCodeExpires == nil || pr.VerificationCodeExpires.Before(now) {
|
|
return fmt.Errorf("verification code expired")
|
|
}
|
|
|
|
// Check if max attempts reached
|
|
if pr.VerificationAttempts >= maxVerificationAttempts {
|
|
return fmt.Errorf("max verification attempts reached")
|
|
}
|
|
|
|
// Check if code matches
|
|
if pr.VerificationCode != req.Code {
|
|
// Increment attempt counter
|
|
if err := tx.Model(&pr).
|
|
Update("verification_attempts", pr.VerificationAttempts+1).Error; err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("invalid verification code")
|
|
}
|
|
|
|
// Code is valid; do not expire it here. Allow completion step to validate within original expiry window.
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
if err.Error() == "record not found" {
|
|
c.JSON(http.StatusNotFound, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Neplatný nebo expirovaný odkaz pro obnovení hesla",
|
|
})
|
|
return
|
|
} else if err.Error() == "verification code expired" {
|
|
c.JSON(http.StatusBadRequest, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Platnost ověřovacího kódu vypršela. Požádejte o nový kód.",
|
|
})
|
|
return
|
|
} else if err.Error() == "max verification attempts reached" {
|
|
c.JSON(http.StatusTooManyRequests, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Překročen maximální počet pokusů. Požádejte o nový kód.",
|
|
})
|
|
return
|
|
} else if err.Error() == "invalid verification code" {
|
|
attemptsLeft := maxVerificationAttempts - pr.VerificationAttempts - 1
|
|
message := fmt.Sprintf("Neplatný ověřovací kód. Zbývající počet pokusů: %d", attemptsLeft)
|
|
c.JSON(http.StatusBadRequest, PasswordResetResponse{
|
|
Success: false,
|
|
Message: message,
|
|
})
|
|
return
|
|
}
|
|
|
|
logger.Error("Error verifying reset code: error=%v email=%s", err, emailAddr)
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Došlo k chybě při ověřování kódu",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Return success with the reset token
|
|
c.JSON(http.StatusOK, PasswordResetResponse{
|
|
Success: true,
|
|
Message: "Kód byl úspěšně ověřen. Můžete nastavit nové heslo.",
|
|
NextStep: "reset_password",
|
|
})
|
|
}
|
|
|
|
// CompletePasswordReset verifies the reset code and updates the password
|
|
func (pc *PasswordController) CompletePasswordReset(c *gin.Context) {
|
|
var req CompletePasswordResetRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Neplatný formát požadavku: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
emailAddr := strings.TrimSpace(strings.ToLower(req.Email))
|
|
if emailAddr == "" || !strings.Contains(emailAddr, "@") {
|
|
c.JSON(http.StatusBadRequest, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Neplatná e-mailová adresa",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Find the user
|
|
var user models.User
|
|
if err := pc.DB.Where("LOWER(email) = LOWER(?)", emailAddr).First(&user).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Uživatel s tímto e-mailem nebyl nalezen",
|
|
})
|
|
return
|
|
}
|
|
logger.Error("Database error when finding user: error=%v email=%s", err, emailAddr)
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Došlo k chybě při obnovování hesla",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Verify the reset code and get the reset record
|
|
var pr models.PasswordReset
|
|
now := time.Now()
|
|
err := pc.DB.Transaction(func(tx *gorm.DB) error {
|
|
// Find the active reset record
|
|
if err := tx.Where("user_id = ? AND used_at IS NULL AND expires_at > ?", user.ID, now).
|
|
Order("created_at DESC").First(&pr).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if verification code matches and is not expired
|
|
if pr.VerificationCode != req.Code ||
|
|
pr.VerificationCodeExpires == nil ||
|
|
pr.VerificationCodeExpires.Before(now) {
|
|
return fmt.Errorf("invalid or expired verification code")
|
|
}
|
|
|
|
// Update user's password
|
|
hashedPassword, err := utils.HashPassword(req.NewPassword)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
|
|
if err := tx.Model(&user).Update("password", hashedPassword).Error; err != nil {
|
|
return fmt.Errorf("failed to update password: %w", err)
|
|
}
|
|
|
|
// Mark the reset record as used
|
|
now := time.Now()
|
|
pr.UsedAt = &now
|
|
return tx.Save(&pr).Error
|
|
})
|
|
|
|
if err != nil {
|
|
if err.Error() == "record not found" || err.Error() == "invalid or expired verification code" {
|
|
c.JSON(http.StatusBadRequest, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Neplatný nebo expirovaný ověřovací kód. Zkuste to znovu.",
|
|
})
|
|
return
|
|
}
|
|
|
|
logger.Error("Error completing password reset: error=%v email=%s", err, emailAddr)
|
|
c.JSON(http.StatusInternalServerError, PasswordResetResponse{
|
|
Success: false,
|
|
Message: "Došlo k chybě při obnovování hesla",
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, PasswordResetResponse{
|
|
Success: true,
|
|
Message: "Vaše heslo bylo úspěšně změněno. Nyní se můžete přihlásit.",
|
|
NextStep: "login",
|
|
})
|
|
}
|
|
|
|
// AdminSendResetByID sends a reset email for a specific user ID using override SMTP
|
|
func (pc *PasswordController) AdminSendResetByID(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil || id <= 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
var user models.User
|
|
if err := pc.DB.First(&user, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
|
} else {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check if email service is available
|
|
if pc.EmailService == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"error": "Email service not configured",
|
|
"message": "SMTP settings must be configured in the admin panel or via environment variables (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM) before sending password reset emails.",
|
|
})
|
|
return
|
|
}
|
|
|
|
var b [16]byte
|
|
_, _ = cryptorand.Read(b[:])
|
|
token := hex.EncodeToString(b[:])
|
|
pr := &models.PasswordReset{
|
|
UserID: user.ID,
|
|
Token: token,
|
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
|
IP: c.ClientIP(),
|
|
UserAgent: c.Request.UserAgent(),
|
|
}
|
|
if err := pc.DB.Create(pr).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create reset record"})
|
|
return
|
|
}
|
|
base := strings.TrimSuffix(config.AppConfig.FrontendBaseURL, "/")
|
|
if base == "" {
|
|
base = "http://localhost:3000"
|
|
}
|
|
link := base + "/reset-password?token=" + token
|
|
if se, ok := pc.EmailService.(interface {
|
|
SendPasswordReset(to, link string, useOverride bool) error
|
|
}); ok {
|
|
if err := se.SendPasswordReset(user.Email, link, true); err != nil {
|
|
logger.Error("Failed to send password reset email: error=%v user_id=%d", err, user.ID)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "failed to send reset email",
|
|
"message": "SMTP configuration may be invalid or incomplete. Check SMTP_HOST, SMTP_USER, SMTP_PASSWORD, SMTP_FROM in .env file or configure in admin settings.",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
} else {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Email service does not support password reset"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|
|
|
|
func NewPasswordController(db *gorm.DB, es email.EmailService) *PasswordController {
|
|
return &PasswordController{DB: db, EmailService: es}
|
|
}
|
|
|
|
type forgotPasswordRequest struct {
|
|
Email string `json:"email" binding:"required"`
|
|
}
|
|
|
|
// ForgotPassword creates a reset token and sends email
|
|
func (pc *PasswordController) ForgotPassword(c *gin.Context) {
|
|
var req forgotPasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
emailAddr := strings.TrimSpace(strings.ToLower(req.Email))
|
|
if emailAddr == "" || !strings.Contains(emailAddr, "@") {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid email"})
|
|
return
|
|
}
|
|
|
|
// Lookup user; don't reveal whether exists
|
|
var user models.User
|
|
if err := pc.DB.Where("LOWER(email) = LOWER(?)", emailAddr).First(&user).Error; err != nil {
|
|
// Respond success to avoid user enumeration
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
return
|
|
}
|
|
|
|
// Create token
|
|
var b [16]byte
|
|
_, _ = cryptorand.Read(b[:])
|
|
token := hex.EncodeToString(b[:])
|
|
pr := &models.PasswordReset{
|
|
UserID: user.ID,
|
|
Token: token,
|
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
|
IP: c.ClientIP(),
|
|
UserAgent: c.Request.UserAgent(),
|
|
}
|
|
if err := pc.DB.Create(pr).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create reset"})
|
|
return
|
|
}
|
|
|
|
// Build reset link
|
|
base := strings.TrimSuffix(config.AppConfig.FrontendBaseURL, "/")
|
|
link := base + "/reset-password?token=" + token
|
|
|
|
// Send email via default SMTP
|
|
if se, ok := pc.EmailService.(interface {
|
|
SendPasswordReset(to, link string, useOverride bool) error
|
|
}); ok {
|
|
if err := se.SendPasswordReset(user.Email, link, false); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send reset email"})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|
|
|
|
type resetPasswordRequest struct {
|
|
Token string `json:"token" binding:"required"`
|
|
NewPassword string `json:"new_password" binding:"required,min=8"`
|
|
}
|
|
|
|
// ResetPassword consumes a token and sets a new password
|
|
func (pc *PasswordController) ResetPassword(c *gin.Context) {
|
|
var req resetPasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var pr models.PasswordReset
|
|
if err := pc.DB.Where("token = ?", req.Token).First(&pr).Error; err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid token"})
|
|
return
|
|
}
|
|
if pr.UsedAt != nil || time.Now().After(pr.ExpiresAt) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "token expired or used"})
|
|
return
|
|
}
|
|
var user models.User
|
|
if err := pc.DB.First(&user, pr.UserID).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "user not found"})
|
|
return
|
|
}
|
|
|
|
hashed, err := utils.HashPassword(req.NewPassword)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash"})
|
|
return
|
|
}
|
|
user.Password = hashed
|
|
if err := pc.DB.Save(&user).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"})
|
|
return
|
|
}
|
|
now := time.Now()
|
|
pr.UsedAt = &now
|
|
_ = pc.DB.Save(&pr).Error
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|
|
|
|
type adminSendResetRequest struct {
|
|
Email string `json:"email" binding:"required"`
|
|
}
|
|
|
|
// AdminSendReset sends a reset email using special SMTP when key matches
|
|
func (pc *PasswordController) AdminSendReset(c *gin.Context) {
|
|
var req adminSendResetRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
emailAddr := strings.TrimSpace(strings.ToLower(req.Email))
|
|
var user models.User
|
|
if err := pc.DB.Where("LOWER(email) = LOWER(?)", emailAddr).First(&user).Error; err != nil {
|
|
// respond ok regardless
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
return
|
|
}
|
|
// Create token always fresh
|
|
var b [16]byte
|
|
_, _ = cryptorand.Read(b[:])
|
|
token := hex.EncodeToString(b[:])
|
|
pr := &models.PasswordReset{
|
|
UserID: user.ID,
|
|
Token: token,
|
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
|
IP: c.ClientIP(),
|
|
UserAgent: c.Request.UserAgent(),
|
|
}
|
|
if err := pc.DB.Create(pr).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed"})
|
|
return
|
|
}
|
|
base := strings.TrimSuffix(config.AppConfig.FrontendBaseURL, "/")
|
|
link := base + "/reset-password?token=" + token
|
|
if se, ok := pc.EmailService.(interface {
|
|
SendPasswordReset(to, link string, useOverride bool) error
|
|
}); ok {
|
|
if err := se.SendPasswordReset(user.Email, link, true); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send reset email"})
|
|
return
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|