- login.html: corregir link roto /password/reset → /request-password-reset
- request-password-reset.html: reescribir — pide email, AJAX, diseño de la app
- RequestPasswordResetPost: buscar por email (GetUserByEmail), no revela si
el correo existe o no (misma respuesta siempre — seguridad)
- password-reset.html: reescribir — eliminar campo nombre_usuario redundante,
validación de contraseñas en cliente, diseño consistente con el resto del app
- PasswordResetPost: usar c.Locals("email") del token (ya validado por
middleware) en vez de nombre_usuario del form
- password_reset.go middleware: ampliar expiración de token de 5 min a 1 hora
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
284 lines
8.2 KiB
Go
Executable File
284 lines
8.2 KiB
Go
Executable File
package controllers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"github.com/sujit-baniya/fiber-boilerplate/config"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/auth"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Función para renderizar la página de registro
|
|
func RegisterGet(c *fiber.Ctx) error {
|
|
data := getFlashData(c)
|
|
data["title"] = "Register | "
|
|
if err := c.Render("auth/register", data, "layouts/landing"); err != nil { //nolint:wsl
|
|
panic(err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Función para manejar la solicitud de registro
|
|
func RegisterPost(c *fiber.Ctx) error {
|
|
register := c.Locals("register").(models.RegisterForm)
|
|
user, err := register.Signup()
|
|
if err != nil {
|
|
return app.Http.Flash.WithError(c, fiber.Map{
|
|
"error": true,
|
|
"message": "Error on register request: " + err.Error(),
|
|
}).Redirect("/register")
|
|
}
|
|
store := app.Http.Session.Get(c) // obtener/crear nueva sesión
|
|
store.Set("user_id", user.ID) // guardar en el almacenamiento
|
|
_ = store.Save()
|
|
|
|
go services.SendConfirmationEmail(user.Email, services.GetPublicURL(), user.NombreUsuario)
|
|
return c.Redirect("/")
|
|
}
|
|
|
|
// Función para verificar el correo electrónico registrado
|
|
func VerifyRegisteredEmail(c *fiber.Ctx) error {
|
|
|
|
return c.Redirect("/app")
|
|
}
|
|
|
|
// Función para reenviar el correo de confirmación
|
|
func ResendConfirmEmail(c *fiber.Ctx) error {
|
|
user, _ := auth.User(c)
|
|
go services.SendConfirmationEmail(user.Email, services.GetPublicURL(), user.NombreUsuario)
|
|
return c.Redirect("/")
|
|
}
|
|
|
|
func ReenvioEmail(c *fiber.Ctx) error {
|
|
usuarioID := c.Params("id") // Usa Params para obtener el parámetro de la ruta
|
|
user, err := models.GetUserById(usuarioID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
|
"success": false,
|
|
"message": "Usuario no encontrado: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Reenvío del correo electrónico de forma asíncrona
|
|
go services.SendConfirmationEmail(user.Email, services.GetPublicURL(), user.NombreUsuario)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"success": true,
|
|
"message": "Email reenviado correctamente",
|
|
})
|
|
}
|
|
|
|
// Función para manejar la solicitud de restablecimiento de contraseña
|
|
func RequestPasswordResetPost(c *fiber.Ctx) error {
|
|
email := strings.TrimSpace(c.FormValue("email"))
|
|
if email == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"success": false,
|
|
"message": "El correo electrónico es requerido.",
|
|
})
|
|
}
|
|
|
|
// Intentar enviar el correo solo si el usuario existe — mismo mensaje en ambos casos por seguridad
|
|
user, err := models.GetUserByEmail(email)
|
|
if err == nil && user != nil && user.Email != "" {
|
|
log.Println("[Reset] Enviando enlace de recuperación a:", email)
|
|
go services.SendPasswordResetEmail(user.Email, services.GetPublicURL())
|
|
} else {
|
|
log.Println("[Reset] Correo no encontrado (no se revela al cliente):", email)
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"success": true,
|
|
"message": "Si el correo está registrado, recibirás instrucciones en los próximos minutos.",
|
|
})
|
|
}
|
|
|
|
|
|
// todo: Generar contrasenas aleatorias /do/generate-password
|
|
func generatePassword(length int) (string, error) {
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&"
|
|
var password strings.Builder
|
|
for i := 0; i < length; i++ {
|
|
randomIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
password.WriteByte(charset[randomIndex.Int64()])
|
|
}
|
|
return password.String(), nil
|
|
}
|
|
|
|
func hashPassword(password string) (string, error) {
|
|
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(hashed), nil
|
|
}
|
|
|
|
func updatePassword(nombreUsuario, plainPassword string) error {
|
|
var user models.RegisterForm
|
|
|
|
result := app.Http.Database.DB.Where("nombre_usuario = ?", nombreUsuario).First(&user)
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return errors.New("codigo no pertenece a ningun usuario")
|
|
}
|
|
|
|
if user.Password != "" {
|
|
return errors.New("este usuario ya cuenta con una contraseña")
|
|
}
|
|
|
|
hashedPassword, err := hashPassword(plainPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user.Password = hashedPassword
|
|
if err := app.Http.Database.DB.Save(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GeneratePasswordPost(c *fiber.Ctx) error {
|
|
// Parsear el cuerpo de la solicitud
|
|
var body struct {
|
|
NombreUsuario string `json:"nombre_usuario"`
|
|
}
|
|
if err := c.BodyParser(&body); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"success": false,
|
|
"message": "Error al analizar el cuerpo de la solicitud",
|
|
})
|
|
}
|
|
|
|
if body.NombreUsuario == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"success": false,
|
|
"message": "El codigo de usuario es requerido",
|
|
})
|
|
}
|
|
|
|
// Generar la contraseña
|
|
password, err := generatePassword(12)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"success": false,
|
|
"message": "Error generando contraseña",
|
|
})
|
|
}
|
|
|
|
// Actualizar la contraseña en la base de datos
|
|
if err := updatePassword(body.NombreUsuario, password); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"success": false,
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
|
|
// Responder con la contraseña generada
|
|
return c.JSON(fiber.Map{
|
|
"success": true,
|
|
"message": "Contrasena generada exitosamente",
|
|
"password": password,
|
|
})
|
|
}
|
|
|
|
// Función para renderizar la página de restablecimiento de contraseña
|
|
func PasswordReset(c *fiber.Ctx) error {
|
|
token := c.Query("t")
|
|
if err := c.Render("auth/password-reset", fiber.Map{
|
|
"Title": "Password Reset",
|
|
"Token": token,
|
|
}, "layouts/landing"); err != nil {
|
|
panic(err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func PasswordResetPost(c *fiber.Ctx) error {
|
|
// El email fue validado y extraído del token por el middleware ValidatePasswordResetPost
|
|
emailLocal := c.Locals("email")
|
|
if emailLocal == nil {
|
|
return c.Status(fiber.StatusBadRequest).Render("auth/password-reset", fiber.Map{
|
|
"Token": c.Params("token"),
|
|
"error": "Token inválido. Solicita un nuevo enlace.",
|
|
}, "layouts/landing")
|
|
}
|
|
email := fmt.Sprintf("%v", emailLocal)
|
|
|
|
password := c.FormValue("password")
|
|
cPassword := c.FormValue("c_password")
|
|
|
|
if password == "" || len(password) < 6 {
|
|
return c.Render("auth/password-reset", fiber.Map{
|
|
"Token": c.Params("token"),
|
|
"error": "La contraseña debe tener al menos 6 caracteres.",
|
|
}, "layouts/landing")
|
|
}
|
|
if password != cPassword {
|
|
return c.Render("auth/password-reset", fiber.Map{
|
|
"Token": c.Params("token"),
|
|
"error": "Las contraseñas no coinciden.",
|
|
}, "layouts/landing")
|
|
}
|
|
|
|
user, err := models.GetUserByEmail(email)
|
|
if err != nil || user == nil {
|
|
return c.Render("auth/password-reset", fiber.Map{
|
|
"Token": c.Params("token"),
|
|
"error": "No se encontró una cuenta asociada a este enlace.",
|
|
}, "layouts/landing")
|
|
}
|
|
|
|
hash := config.Hash{}
|
|
hashedPassword, err := hash.Create(password)
|
|
if err != nil {
|
|
return c.Render("auth/password-reset", fiber.Map{
|
|
"Token": c.Params("token"),
|
|
"error": "Error al procesar la contraseña. Intenta de nuevo.",
|
|
}, "layouts/landing")
|
|
}
|
|
|
|
if err := models.UpdateUserPassword(user.ID, hashedPassword); err != nil {
|
|
return c.Render("auth/password-reset", fiber.Map{
|
|
"Token": c.Params("token"),
|
|
"error": "No se pudo actualizar la contraseña. Intenta de nuevo.",
|
|
}, "layouts/landing")
|
|
}
|
|
|
|
return c.Redirect("/login")
|
|
}
|
|
|
|
// Función para renderizar la página de solicitud de restablecimiento de contraseña
|
|
func RequestPasswordReset(c *fiber.Ctx) error {
|
|
data := getFlashData(c)
|
|
data["title"] = "Password Reset | "
|
|
if err := c.Render("auth/request-password-reset", data, "layouts/landing"); err != nil { //nolint:wsl
|
|
panic(err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TokenVencido(c *fiber.Ctx) error {
|
|
// Renderiza la vista con el layout layouts/main
|
|
if err := c.Render("token_vencido", nil, "layouts/landing"); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"message": "Error rendering template",
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
return nil
|
|
}
|