From 6e27032ac6e708492f7f1c84b676a9a4133291bd Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 19 May 2026 15:25:53 -0500 Subject: [PATCH] =?UTF-8?q?feat(portal):=20agregar=20flujo=20de=20recupera?= =?UTF-8?q?ci=C3=B3n=20de=20contrase=C3=B1a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- main.go | 2 + pkg/models/portal_user.go | 55 +++++++++++++ pkg/services/mail_service.go | 25 ++++++ resources/views/portal/forgot_password.html | 48 +++++++++++ resources/views/portal/login.html | 11 +++ resources/views/portal/reset_password.html | 87 ++++++++++++++++++++ rest/controllers/portal_controller.go | 89 ++++++++++++++++++++- rest/routes/portal.go | 4 + 8 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 resources/views/portal/forgot_password.html create mode 100644 resources/views/portal/reset_password.html diff --git a/main.go b/main.go index b2b7441..b2fda94 100755 --- a/main.go +++ b/main.go @@ -71,6 +71,8 @@ func main() { // Partner &models.PartnerRecurso{}, &models.PartnerComunicado{}, + // Portal: recuperación de contraseña + &models.PortalPasswordResetToken{}, ) // Seed automático (idempotente) de módulos del sistema migrations.SeedRenovaciones() diff --git a/pkg/models/portal_user.go b/pkg/models/portal_user.go index bdf51fc..90f3892 100644 --- a/pkg/models/portal_user.go +++ b/pkg/models/portal_user.go @@ -1,7 +1,10 @@ package models import ( + "crypto/rand" + "encoding/hex" "errors" + "time" "github.com/sujit-baniya/fiber-boilerplate/app" "gorm.io/gorm" @@ -196,6 +199,58 @@ func GetClienteIDsForPortalUser(u *PortalUser) []uint { return []uint{} } +// ─── PortalPasswordResetToken ──────────────────────────────────────────────── + +type PortalPasswordResetToken struct { + ID uint `gorm:"primaryKey;autoIncrement"` + PortalUserID uint `gorm:"column:portal_user_id;index;not null"` + Token string `gorm:"column:token;uniqueIndex;not null"` + ExpiresAt time.Time `gorm:"column:expires_at;not null"` + Used bool `gorm:"column:used;default:false"` + CreatedAt time.Time +} + +func (PortalPasswordResetToken) TableName() string { return "portal_password_reset_tokens" } + +// CreatePortalResetToken genera un token aleatorio seguro de 32 bytes, lo persiste y lo retorna. +func CreatePortalResetToken(portalUserID uint) (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + token := hex.EncodeToString(b) + record := &PortalPasswordResetToken{ + PortalUserID: portalUserID, + Token: token, + ExpiresAt: time.Now().Add(1 * time.Hour), + } + if err := app.Http.Database.DB.Create(record).Error; err != nil { + return "", err + } + return token, nil +} + +// GetValidPortalResetToken busca el token, verifica que no esté usado ni vencido. +func GetValidPortalResetToken(token string) (*PortalPasswordResetToken, error) { + var record PortalPasswordResetToken + err := app.Http.Database.DB.Where("token = ?", token).First(&record).Error + if err != nil { + return nil, errors.New("token inválido") + } + if record.Used { + return nil, errors.New("este enlace ya fue utilizado") + } + if time.Now().After(record.ExpiresAt) { + return nil, errors.New("el enlace ha expirado, solicita uno nuevo") + } + return &record, nil +} + +// MarkPortalResetTokenUsed marca el token como utilizado. +func MarkPortalResetTokenUsed(id uint) error { + return app.Http.Database.DB.Model(&PortalPasswordResetToken{}).Where("id = ?", id).Update("used", true).Error +} + // CheckPortalLogin valida credenciales y retorna el portal user. func CheckPortalLogin(email, password string) (*PortalUser, error) { u, err := GetPortalUserByEmail(email) diff --git a/pkg/services/mail_service.go b/pkg/services/mail_service.go index 50f37e3..b165d9e 100755 --- a/pkg/services/mail_service.go +++ b/pkg/services/mail_service.go @@ -78,6 +78,31 @@ func GeneratePasswordResetURL(email string, baseURL string) string { return uri } +// SendPortalPasswordResetEmail envía al usuario del portal un enlace para restablecer su contraseña. +func SendPortalPasswordResetEmail(email, nombre, resetLink string) { + htmlBody := fmt.Sprintf(` +
+Portal de Clientes · U-site
+Hola %s,
+Recibimos una solicitud para restablecer la contraseña de tu cuenta. Haz clic en el siguiente botón para continuar:
+ Restablecer contraseña +Este enlace es válido por 1 hora. Si no solicitaste este cambio, puedes ignorar este mensaje.
+© 2025 U-site — Todos los derechos reservados
+Te enviaremos un enlace para restablecerla
++ ← Volver al inicio de sesión +
+ ++ © 2025 U-site — Todos los derechos reservados +
+diff --git a/resources/views/portal/reset_password.html b/resources/views/portal/reset_password.html new file mode 100644 index 0000000..a868398 --- /dev/null +++ b/resources/views/portal/reset_password.html @@ -0,0 +1,87 @@ + +
Elige una contraseña segura para tu cuenta
+Este enlace no es válido o ya fue utilizado.
+ + Solicitar nuevo enlace + ++ ← Volver al inicio de sesión +
+ ++ © 2025 U-site — Todos los derechos reservados +
+