feat(users): reemplazar confirmación por envío de credenciales con link de reset

- Eliminar envío automático de confirmación al crear usuario
- Nuevo botón en cada fila: enviar credenciales (usuario + link establecer contraseña)
- POST /app/users/:id/credenciales → SendUserCredencialesEmail
- Email incluye username, link al login y link para establecer contraseña

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-06 21:57:36 -05:00
co-authored by Claude Sonnet 4.6
parent bf3cbd3606
commit 107178aa97
4 changed files with 71 additions and 28 deletions
+34
View File
@@ -50,6 +50,40 @@ func SendConfirmationEmail(email string, baseURL string, nombreUsuario string) {
}
}
// SendUserCredencialesEmail envía al usuario admin su nombre de usuario y un link para establecer contraseña.
func SendUserCredencialesEmail(email, nombre, nombreUsuario string) {
resetEmail := fmt.Sprintf("%s-reset-%d", email, time.Now().Unix())
resetLink := GeneratePasswordResetURL(resetEmail, GetPublicURL())
loginURL := absAppURL("/login")
htmlBody := fmt.Sprintf(`<!DOCTYPE html>
<html><body style="font-family:Inter,sans-serif;background:#f1f5f9;padding:32px">
<div style="max-width:480px;margin:0 auto;background:#fff;border-radius:16px;padding:32px;border:1px solid #e2e8f0">
<div style="text-align:center;margin-bottom:24px">
<div style="width:48px;height:48px;border-radius:50%%;background:#8eb02f;display:inline-flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:20px">U</div>
<h2 style="margin:12px 0 4px;color:#1e293b">Bienvenido a U-site</h2>
<p style="color:#64748b;font-size:14px;margin:0">Panel de administración</p>
</div>
<p style="color:#334155;font-size:15px">Hola <strong>%s</strong>,</p>
<p style="color:#334155;font-size:14px">Tu cuenta ha sido creada. Estos son tus datos de acceso:</p>
<div style="background:#f8fafc;border-radius:10px;padding:16px;margin:20px 0;border:1px solid #e2e8f0">
<p style="margin:0 0 8px;font-size:13px;color:#64748b"><strong>Usuario:</strong> %s</p>
<p style="margin:0;font-size:13px;color:#64748b"><strong>Acceso:</strong> <a href="%s" style="color:#8eb02f">%s</a></p>
</div>
<p style="color:#334155;font-size:14px">Establece tu contraseña haciendo clic en el siguiente botón:</p>
<a href="%s" style="display:block;text-align:center;background:#8eb02f;color:#fff;padding:12px 24px;border-radius:10px;font-weight:600;text-decoration:none;font-size:15px;margin:20px 0">Establecer contraseña</a>
<p style="color:#64748b;font-size:13px">El enlace es válido por <strong>1 hora</strong>.</p>
<p style="font-size:12px;color:#94a3b8;margin-top:24px;text-align:center">© U-site — Todos los derechos reservados</p>
</div>
</body></html>`, nombre, nombreUsuario, loginURL, loginURL, resetLink)
if err := app.Http.Mail.Send(email, "Bienvenido a U-site — Tus credenciales de acceso", htmlBody); err != nil {
log.Printf("[MAIL] Error enviando credenciales a %s: %v", email, err)
} else {
log.Printf("[MAIL] Credenciales enviadas a %s", email)
}
}
func GenerateConfirmURL(nombreUsuario string, baseURL string) string {
token := utils.Encrypt(nombreUsuario, app.Http.Server.Key)
uri := fmt.Sprintf("%s/do/verify-email?t=%s", baseURL, token)
+20 -25
View File
@@ -82,16 +82,15 @@
<!-- Círculo verde -->
</template>
<template x-if="!user.email_verified">
<!-- Botón Enviar verificación -->
<button @click="enviarVerificación(user.ID)" title="Reenviar validación">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-5 text-orange-400">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
</svg>
</button>
</template>
<!-- Botón Enviar credenciales -->
<button @click="enviarCredenciales(user.ID, user.email)" title="Enviar credenciales por email"
x-show="user.email">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-5 text-blue-500">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
</svg>
</button>
</td>
<td class="py-3 text-xs px-4 border-b flex justify-between space-x-1">
<!-- Botón de Ver -->
@@ -547,22 +546,18 @@
});
},
enviarVerificación($id) {
this.Id = $id;
async enviarCredenciales($id, $email) {
if (!confirm(`¿Enviar credenciales de acceso a ${$email}?`)) return;
this.loading = true;
fetch(`/app/verificacion/${this.Id}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
})
.then(() => {
alert('Se ha enviado un correo de verificación');
this.closeModal();
this.loadRegister();
})
.catch(error => console.error('Error:', error))
.finally(() => {
this.loading = false; // Ocultar el GIF de carga
});
try {
const r = await fetch(`/app/users/${$id}/credenciales`, { method: 'POST' });
const data = await r.json();
alert(data.message || 'Credenciales enviadas');
} catch (e) {
alert('Error al enviar credenciales');
} finally {
this.loading = false;
}
}
}));
});
+16 -3
View File
@@ -486,9 +486,6 @@ func CreateUserGas(c *fiber.Ctx) error {
})
}
// Envía un correo de confirmación
go services.SendConfirmationEmail(m.Email, services.GetPublicURL(), m.NombreUsuario)
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"message": "Usuario creado con éxito",
"error": false,
@@ -555,3 +552,19 @@ func createPassword(c *fiber.Ctx, UserId uint) error {
"error": false,
})
}
func EnviarCredencialesUsuario(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
user, err := models.FindUserByID(uint(id))
if err != nil || user == nil {
return c.Status(404).JSON(fiber.Map{"error": "Usuario no encontrado"})
}
if user.Email == "" {
return c.Status(400).JSON(fiber.Map{"error": "El usuario no tiene email registrado"})
}
go services.SendUserCredencialesEmail(user.Email, user.Name, user.NombreUsuario)
return c.JSON(fiber.Map{"ok": true, "message": "Credenciales enviadas a " + user.Email})
}
+1
View File
@@ -54,6 +54,7 @@ func UserRoutes(app fiber.Router) {
protected.Put("/password/:id", controllers.UpdatePassword) // Actualizar
protected.Delete("/users/:id", controllers.DeleteUser) // Eliminar
protected.Get("/user/:id", controllers.GetUser) // Buscar un usuario
protected.Post("/users/:id/credenciales", controllers.EnviarCredencialesUsuario) // Enviar credenciales
// Rutas de conexiones ssh
protected.Get("/conexion_ssh", middlewares.MenuMiddleware, controllers.ConxSsh) // Renderizar la vista