mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,629 @@
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user