feat(portal): agregar flujo de recuperación de contraseña
- Nuevo modelo PortalPasswordResetToken con token seguro (32 bytes, 1h vigencia) - AutoMigrate del nuevo modelo en main.go - SendPortalPasswordResetEmail con diseño consistente al app - Handlers: PortalForgotPasswordPage/Post y PortalResetPasswordPost/Page - Rutas públicas GET/POST /portal/forgot-password y /portal/reset-password - Vistas forgot_password.html y reset_password.html con validación JS - Enlace ¿Olvidaste tu contraseña? en login.html + soporte mensaje success Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
b5e6a9285f
commit
6e27032ac6
@@ -25,7 +25,8 @@ func PortalLoginPage(c *fiber.Ctx) error {
|
||||
return c.Redirect("/portal/dashboard")
|
||||
}
|
||||
return c.Render("portal/login", fiber.Map{
|
||||
"error": c.Query("error"),
|
||||
"error": c.Query("error"),
|
||||
"success": c.Query("success"),
|
||||
}, "layouts/portal_public")
|
||||
}
|
||||
|
||||
@@ -47,6 +48,92 @@ func PortalLogout(c *fiber.Ctx) error {
|
||||
return c.Redirect("/portal/login")
|
||||
}
|
||||
|
||||
// ─── Recuperar contraseña ────────────────────────────────────────────────────
|
||||
|
||||
func PortalForgotPasswordPage(c *fiber.Ctx) error {
|
||||
return c.Render("portal/forgot_password", fiber.Map{
|
||||
"success": c.Query("success"),
|
||||
"error": c.Query("error"),
|
||||
}, "layouts/portal_public")
|
||||
}
|
||||
|
||||
func PortalForgotPasswordPost(c *fiber.Ctx) error {
|
||||
email := strings.TrimSpace(c.FormValue("email"))
|
||||
|
||||
// Respuesta genérica siempre para no revelar si el email existe
|
||||
successMsg := "Si el correo está registrado, recibirás un enlace para restablecer tu contraseña."
|
||||
|
||||
u, err := models.GetPortalUserByEmail(email)
|
||||
if err == nil && u != nil && u.Activo {
|
||||
token, err := models.CreatePortalResetToken(u.ID)
|
||||
if err == nil {
|
||||
resetLink := services.GetPublicURL() + "/portal/reset-password?t=" + token
|
||||
services.SendPortalPasswordResetEmail(u.Email, u.Nombre, resetLink)
|
||||
} else {
|
||||
log.Printf("[Portal] Error creando token de reseteo para %s: %v", email, err)
|
||||
}
|
||||
}
|
||||
|
||||
return c.Redirect("/portal/forgot-password?success=" + url.QueryEscape(successMsg))
|
||||
}
|
||||
|
||||
func PortalResetPasswordPage(c *fiber.Ctx) error {
|
||||
token := c.Query("t")
|
||||
if token == "" {
|
||||
return c.Redirect("/portal/login")
|
||||
}
|
||||
_, err := models.GetValidPortalResetToken(token)
|
||||
if err != nil {
|
||||
return c.Render("portal/reset_password", fiber.Map{
|
||||
"error": err.Error(),
|
||||
"token": "",
|
||||
}, "layouts/portal_public")
|
||||
}
|
||||
return c.Render("portal/reset_password", fiber.Map{
|
||||
"token": token,
|
||||
"error": "",
|
||||
}, "layouts/portal_public")
|
||||
}
|
||||
|
||||
func PortalResetPasswordPost(c *fiber.Ctx) error {
|
||||
token := c.FormValue("token")
|
||||
password := c.FormValue("password")
|
||||
confirm := c.FormValue("confirm")
|
||||
|
||||
renderError := func(msg string) error {
|
||||
return c.Render("portal/reset_password", fiber.Map{
|
||||
"token": token,
|
||||
"error": msg,
|
||||
}, "layouts/portal_public")
|
||||
}
|
||||
|
||||
if token == "" || password == "" || confirm == "" {
|
||||
return renderError("Todos los campos son obligatorios.")
|
||||
}
|
||||
if password != confirm {
|
||||
return renderError("Las contraseñas no coinciden.")
|
||||
}
|
||||
if len(password) < 8 {
|
||||
return renderError("La contraseña debe tener al menos 8 caracteres.")
|
||||
}
|
||||
|
||||
record, err := models.GetValidPortalResetToken(token)
|
||||
if err != nil {
|
||||
return renderError(err.Error())
|
||||
}
|
||||
|
||||
hashed, err := app.Http.Hash.Create(password)
|
||||
if err != nil {
|
||||
return renderError("Error al procesar la contraseña. Intenta de nuevo.")
|
||||
}
|
||||
if err := models.UpdatePortalUserPassword(record.PortalUserID, hashed); err != nil {
|
||||
return renderError("Error al actualizar la contraseña. Intenta de nuevo.")
|
||||
}
|
||||
_ = models.MarkPortalResetTokenUsed(record.ID)
|
||||
|
||||
return c.Redirect("/portal/login?success=" + url.QueryEscape("Contraseña actualizada. Ya puedes ingresar."))
|
||||
}
|
||||
|
||||
// ─── Dashboard ────────────────────────────────────────────────────────────────
|
||||
|
||||
func PortalDashboard(c *fiber.Ctx) error {
|
||||
|
||||
@@ -11,6 +11,10 @@ func PortalRoutes(app fiber.Router) {
|
||||
app.Get("/portal/login", controllers.PortalLoginPage)
|
||||
app.Post("/portal/login", controllers.PortalLoginPost)
|
||||
app.Get("/portal/logout", controllers.PortalLogout)
|
||||
app.Get("/portal/forgot-password", controllers.PortalForgotPasswordPage)
|
||||
app.Post("/portal/forgot-password", controllers.PortalForgotPasswordPost)
|
||||
app.Get("/portal/reset-password", controllers.PortalResetPasswordPage)
|
||||
app.Post("/portal/reset-password", controllers.PortalResetPasswordPost)
|
||||
|
||||
// ─── Rutas protegidas ──────────────────────────────────────────────────────
|
||||
portal := app.Group("/portal").Use(middlewares.PortalAuth())
|
||||
|
||||
Reference in New Issue
Block a user