This commit is contained in:
Lizandro Guarnizo
2026-05-16 21:27:30 -05:00
parent fcc3ff7ca7
commit 4fa506c7d6
11 changed files with 586 additions and 13 deletions
+156 -8
View File
@@ -3,6 +3,8 @@ package controllers
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/gofiber/fiber/v2"
@@ -406,14 +408,16 @@ func PortalGetMiPerfil(c *fiber.Ctx) error {
return c.Status(500).JSON(fiber.Map{"error": "error al obtener perfil"})
}
return c.JSON(fiber.Map{
"id": full.ID,
"nombre": full.Nombre,
"email": full.Email,
"telefono": full.Telefono,
"indicativo": full.Indicativo,
"pais": full.Pais,
"documento": full.Documento,
"empresa": full.Empresa,
"id": full.ID,
"nombre": full.Nombre,
"email": full.Email,
"telefono": full.Telefono,
"indicativo": full.Indicativo,
"pais": full.Pais,
"documento": full.Documento,
"empresa": full.Empresa,
"documento_rut_file": full.DocumentoRutFile,
"documento_rut_nombre": full.DocumentoRutNombre,
})
}
@@ -506,3 +510,147 @@ func PortalCambiarPassword(c *fiber.Ctx) error {
}
return c.JSON(fiber.Map{"ok": true})
}
// POST /portal/mi-perfil/rut-file — sube el documento RUT del usuario autenticado
func PortalSubirRutDocumento(c *fiber.Ctx) error {
u := middlewares.PortalUserFromLocals(c)
if u == nil {
return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
}
file, err := c.FormFile("rut_file")
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Archivo requerido"})
}
if file.Size > 10*1024*1024 {
return c.Status(400).JSON(fiber.Map{"error": "Máximo 10 MB"})
}
ext := strings.ToLower(filepath.Ext(file.Filename))
allowed := map[string]bool{".pdf": true, ".jpg": true, ".jpeg": true, ".png": true, ".webp": true}
if !allowed[ext] {
return c.Status(400).JSON(fiber.Map{"error": "Solo se permiten PDF, JPG, PNG o WEBP"})
}
dir := fmt.Sprintf("uploads/portal_users/%d", u.ID)
if err := os.MkdirAll(dir, 0755); err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Error al crear directorio"})
}
savePath := filepath.Join(dir, "rut"+ext)
if err := c.SaveFile(file, savePath); err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Error al guardar archivo"})
}
if err := models.UpdatePortalUserRutFile(u.ID, savePath, file.Filename); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true, "nombre": file.Filename})
}
// ─── Telegram portal ──────────────────────────────────────────────────────────
// PortalTelegramInit genera un código de vinculación temporal para el usuario del portal.
func PortalTelegramInit(c *fiber.Ctx) error {
u := middlewares.PortalUserFromLocals(c)
if u == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "No autenticado"})
}
token, err := models.GenerateTelegramPortalToken(u.ID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "No se pudo generar el código"})
}
// Obtener username del bot desde el primer TelegramConfig activo
configs, _ := models.GetAllTelegramConfigs()
botToken := ""
for _, cfg := range configs {
if cfg.Activo && cfg.BotToken != "" {
botToken = cfg.BotToken
break
}
}
botUsername := services.GetBotUsername(botToken)
return c.JSON(fiber.Map{
"token": token.Token,
"bot_username": botUsername,
"bot_link": "https://t.me/" + botUsername,
})
}
// PortalTelegramStatus devuelve si el usuario del portal tiene Telegram vinculado.
func PortalTelegramStatus(c *fiber.Ctx) error {
u := middlewares.PortalUserFromLocals(c)
if u == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "No autenticado"})
}
full, err := models.GetPortalUserByID(u.ID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"linked": full.TelegramChatID != "",
"chat_id": full.TelegramChatID,
})
}
// TelegramPortalWebhook recibe actualizaciones del bot de Telegram y vincula el chat_id
// al usuario del portal que envió el código de verificación.
// Este endpoint debe estar registrado como webhook en el bot: POST /setWebhook?url=.../webhooks/telegram-portal
func TelegramPortalWebhook(c *fiber.Ctx) error {
var update struct {
Message struct {
Chat struct {
ID int64 `json:"id"`
} `json:"chat"`
Text string `json:"text"`
} `json:"message"`
}
if err := c.BodyParser(&update); err != nil {
return c.SendStatus(fiber.StatusOK)
}
chatID := update.Message.Chat.ID
text := strings.TrimSpace(update.Message.Text)
if chatID == 0 || text == "" {
return c.SendStatus(fiber.StatusOK)
}
// Buscar token en el texto: 6 caracteres hex mayúsculas
// Acepta "/vincular A3F9B2" o solo "A3F9B2"
var token string
for _, part := range strings.Fields(text) {
candidate := strings.ToUpper(strings.TrimPrefix(strings.TrimPrefix(part, "/vincular"), "/VINCULAR"))
candidate = strings.TrimSpace(candidate)
if len(candidate) == 6 {
token = candidate
break
}
}
if token == "" {
return c.SendStatus(fiber.StatusOK)
}
t, err := models.FindTelegramPortalToken(token)
if err != nil {
portalTelegramReply(chatID, "❌ Código inválido o expirado. Genera un nuevo código desde el portal.")
return c.SendStatus(fiber.StatusOK)
}
if err := models.UpdatePortalUserTelegramChatID(t.PortalUserID, fmt.Sprintf("%d", chatID)); err != nil {
return c.SendStatus(fiber.StatusOK)
}
models.DeleteTelegramPortalToken(t.PortalUserID)
portalTelegramReply(chatID, "✅ ¡Tu Telegram ha sido vinculado al portal correctamente!\n\nRecibirás notificaciones importantes por este medio.")
return c.SendStatus(fiber.StatusOK)
}
// portalTelegramReply envía un mensaje usando el primer bot activo configurado.
func portalTelegramReply(chatID int64, text string) {
configs, _ := models.GetAllTelegramConfigs()
for _, cfg := range configs {
if cfg.Activo && cfg.BotToken != "" {
svc := &services.TelegramService{BotToken: cfg.BotToken}
_ = svc.SendMessage(chatID, text)
return
}
}
}
@@ -208,3 +208,32 @@ func SendPortalCredentials(c *fiber.Ctx) error {
services.SendPortalCredentialsEmail(u.Email, u.Nombre, password)
return c.JSON(fiber.Map{"ok": true, "message": "Correo enviado a " + u.Email})
}
// GetPortalUsuarioDetail devuelve todos los campos de un portal_user para el panel de detalle.
func GetPortalUsuarioDetail(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"})
}
u, err := models.GetPortalUserByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Usuario no encontrado"})
}
return c.JSON(u)
}
// PortalUsuarioRutFile sirve el archivo RUT/documento del portal user al admin.
func PortalUsuarioRutFile(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"})
}
u, err := models.GetPortalUserByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Usuario no encontrado"})
}
if u.DocumentoRutFile == "" {
return c.Status(404).JSON(fiber.Map{"error": "Este usuario no tiene archivo RUT"})
}
return c.SendFile(u.DocumentoRutFile)
}
+5
View File
@@ -45,4 +45,9 @@ func PortalRoutes(app fiber.Router) {
portal.Get("/mi-perfil", controllers.PortalGetMiPerfil)
portal.Put("/mi-perfil", controllers.PortalUpdateMiPerfil)
portal.Put("/mi-perfil/password", controllers.PortalCambiarPassword)
portal.Post("/mi-perfil/rut-file", controllers.PortalSubirRutDocumento)
// Telegram: vinculación guiada
portal.Post("/mi-perfil/telegram-init", controllers.PortalTelegramInit)
portal.Get("/mi-perfil/telegram-status", controllers.PortalTelegramStatus)
}
+3 -1
View File
@@ -28,7 +28,9 @@ func RutasPublicas(web fiber.Router) {
// ─── Documentación pública ────────────────────────────────────────────────
web.Get("/docs/:saas", controllers.DocsPublicoIndex)
web.Get("/docs/:saas/:slug", controllers.DocsPublicaPagina)
// ─── Webhook de Telegram para vinculación del portal ──────────────────────
// Configurar en el bot: POST https://api.telegram.org/bot{TOKEN}/setWebhook?url={HOST}/webhooks/telegram-portal
web.Post("/webhooks/telegram-portal", controllers.TelegramPortalWebhook)
// ─── Página de estado del sistema (Atlassian Statuspage) ────────────────
web.Get("/status", controllers.StatusPage)
}
+6 -1
View File
@@ -16,7 +16,10 @@ func UserRoutes(app fiber.Router) {
middlewares.LoadUserMiddleware, // Middleware para cargar el usuario
)
// Rutas de la aplicación
// Perfil del usuario autenticado
protected.Get("/profile", middlewares.MenuMiddleware, controllers.Profile)
// Rutas de la aplicación
// web me redireccione a /
protected.Get("/web", func(c *fiber.Ctx) error { return c.Redirect("/", http.StatusSeeOther) })
@@ -249,9 +252,11 @@ func UserRoutes(app fiber.Router) {
protected.Get("/portal-usuarios", middlewares.MenuMiddleware, controllers.PortalUsuariosIndex)
protected.Get("/loadportalusuarios", controllers.LoadPortalUsuarios)
protected.Get("/loadportalusuarios/:id", controllers.GetPortalUsuarioDetail)
protected.Post("/portal-usuarios", controllers.CreatePortalUsuario)
protected.Put("/portal-usuarios/:id", controllers.UpdatePortalUsuario)
protected.Delete("/portal-usuarios/:id", controllers.DeletePortalUsuario)
protected.Get("/portal-usuarios/:id/rut-file", controllers.PortalUsuarioRutFile)
protected.Post("/portal-usuarios/:id/acceso", controllers.AddPortalAcceso)
protected.Delete("/portal-usuarios/:id/acceso/:clienteID", controllers.RemovePortalAcceso)
protected.Post("/portal-usuarios/:id/send-credentials", controllers.SendPortalCredentials)