up
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user