Files
soft_usite/rest/controllers/coolify_controller.go
T
Lizandro GDandClaude Sonnet 4.6 33cfe4fdb0 feat: agente Telegram con IA + Coolify multi-instancia
- Coolify: soporte multi-instancia (CRUD de configs, ?config_id= en todos
  los endpoints, endpoints expandidos para services/databases/teams/envs)
- AiConfig: campos es_agente_bot + telegram_config_id para marcar qué
  config de IA actúa como cerebro del bot administrador
- TelegramAgentHistory + TelegramAgentAuth: historial de conversación por
  chat_id y whitelist de chats autorizados
- Agent Engine: function calling OpenAI-compatible con 25+ herramientas
  (clientes, contratos, contabilidad, proyectos, tickets, tareas,
  Coolify multi-instancia, servidores, monitores URL)
- Webhook POST /webhooks/telegram-agent/:bot_token (público, sin sesión)
- API /api/v2/agent/auth y /api/v2/agent/history para administrar el agente
- AutoMigrate: AiConfig, TelegramAgentHistory, TelegramAgentAuth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-13 18:29:06 +00:00

456 lines
16 KiB
Go

package controllers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
// filterQueryParam elimina un parámetro específico del querystring.
func filterQueryParam(qs, param string) string {
vals, err := url.ParseQuery(qs)
if err != nil {
return qs
}
vals.Del(param)
return vals.Encode()
}
// ─── helpers internos ────────────────────────────────────────────────────────
// coolifyDo ejecuta una petición a la API de Coolify y devuelve el body crudo.
func coolifyDo(method, endpoint string, reqBody io.Reader, contentType string, cfg *models.CoolifyConfig) ([]byte, int, error) {
base := strings.TrimRight(cfg.BaseURL, "/")
url := fmt.Sprintf("%s/api/v1%s", base, endpoint)
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return nil, 0, err
}
req.Header.Set("Authorization", "Bearer "+cfg.ApiToken)
req.Header.Set("Accept", "application/json")
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
return body, resp.StatusCode, nil
}
// coolifyResolveConfig retorna la config a usar según ?config_id= o la activa por defecto.
func coolifyResolveConfig(c *fiber.Ctx) (*models.CoolifyConfig, error) {
if idStr := c.Query("config_id"); idStr != "" {
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
return nil, fmt.Errorf("config_id inválido")
}
return models.GetCoolifyConfigByID(uint(id))
}
return models.GetCoolifyConfig()
}
// coolifyProxy resuelve config, aplica qs de la request y responde al frontend.
func coolifyProxy(c *fiber.Ctx, method, endpoint string) error {
cfg, err := coolifyResolveConfig(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Configura primero la integración Coolify"})
}
if !cfg.Activo {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "La integración Coolify está inactiva"})
}
// Filtrar config_id del querystring para no pasarlo a Coolify
qs := string(c.Request().URI().QueryString())
if qs != "" {
filtered := filterQueryParam(qs, "config_id")
if filtered != "" {
qs = filtered
} else {
qs = ""
}
}
ep := endpoint
if qs != "" {
ep = endpoint + "?" + qs
}
var bodyReader io.Reader
ct := ""
if raw := c.Body(); len(raw) > 0 {
bodyReader = strings.NewReader(string(raw))
ct = c.Get("Content-Type", "application/json")
}
body, status, err := coolifyDo(method, ep, bodyReader, ct, cfg)
if err != nil {
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()})
}
var result json.RawMessage
if err := json.Unmarshal(body, &result); err != nil {
result = json.RawMessage(fmt.Sprintf(`{"raw":%q}`, string(body)))
}
c.Status(status)
return c.JSON(result)
}
// ─── Página principal ─────────────────────────────────────────────────────────
func CoolifyIndex(c *fiber.Ctx) error {
cfg, _ := models.GetCoolifyConfig()
return c.Render("coolify", fiber.Map{
"user": c.Locals("user").(map[string]interface{}),
"modules": c.Locals("userModules"),
"config": cfg,
}, "layouts/main")
}
// ─── Configuración ────────────────────────────────────────────────────────────
func CoolifyGetConfig(c *fiber.Ctx) error {
cfg, err := models.GetCoolifyConfig()
if err != nil {
return c.JSON(fiber.Map{"config": nil})
}
masked := "••••••••"
if cfg.ApiToken == "" {
masked = ""
}
return c.JSON(fiber.Map{"config": fiber.Map{
"ID": cfg.ID,
"nombre": cfg.Nombre,
"base_url": cfg.BaseURL,
"api_token": masked,
"activo": cfg.Activo,
}})
}
func CoolifySaveConfig(c *fiber.Ctx) error {
type Req struct {
Nombre string `json:"nombre"`
BaseURL string `json:"base_url"`
ApiToken string `json:"api_token"`
Activo bool `json:"activo"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
req.BaseURL = strings.TrimRight(strings.TrimSpace(req.BaseURL), "/")
if req.BaseURL == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "base_url es requerido"})
}
cfg, err := models.UpsertCoolifyConfig(req.Nombre, req.BaseURL, req.ApiToken, req.Activo)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true, "config": cfg})
}
// CoolifyTestConnection verifica conectividad con /health (no requiere auth).
func CoolifyTestConnection(c *fiber.Ctx) error {
cfg, err := models.GetCoolifyConfig()
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Sin config"})
}
base := strings.TrimRight(cfg.BaseURL, "/")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(base + "/api/health")
if err != nil {
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()})
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
var result json.RawMessage
if jsonErr := json.Unmarshal(body, &result); jsonErr != nil {
result = json.RawMessage(fmt.Sprintf(`{"raw":%q}`, string(body)))
}
c.Status(resp.StatusCode)
return c.JSON(result)
}
// ─── Applications ─────────────────────────────────────────────────────────────
func CoolifyListApplications(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/applications")
}
func CoolifyGetApplication(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid"))
}
func CoolifyApplicationLogs(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/logs")
}
func CoolifyApplicationEnvs(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/envs")
}
func CoolifyApplicationEnvCreate(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodPost, "/applications/"+c.Params("uuid")+"/envs")
}
func CoolifyApplicationEnvUpdateBulk(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodPatch, "/applications/"+c.Params("uuid")+"/envs")
}
func CoolifyApplicationEnvDelete(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodDelete, "/applications/"+c.Params("uuid")+"/envs/"+c.Params("env_id"))
}
func CoolifyApplicationStart(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/start")
}
func CoolifyApplicationStop(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/stop")
}
func CoolifyApplicationRestart(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/restart")
}
func CoolifyApplicationDeployments(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/deployments")
}
func CoolifyApplicationDeploy(c *fiber.Ctx) error {
// POST /api/v1/deploy?uuid={uuid}&force=false
return coolifyProxy(c, http.MethodPost, "/deploy")
}
// ─── Servers ──────────────────────────────────────────────────────────────────
func CoolifyListServers(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/servers")
}
func CoolifyGetServer(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/servers/"+c.Params("uuid"))
}
func CoolifyServerResources(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/servers/"+c.Params("uuid")+"/resources")
}
func CoolifyServerDomains(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/servers/"+c.Params("uuid")+"/domains")
}
func CoolifyServerValidate(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/servers/"+c.Params("uuid")+"/validate")
}
// ─── Services ─────────────────────────────────────────────────────────────────
func CoolifyListServices(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/services")
}
func CoolifyGetService(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid"))
}
func CoolifyServiceStart(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid")+"/start")
}
func CoolifyServiceStop(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid")+"/stop")
}
func CoolifyServiceRestart(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid")+"/restart")
}
func CoolifyServiceEnvs(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid")+"/envs")
}
func CoolifyServiceEnvUpdateBulk(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodPatch, "/services/"+c.Params("uuid")+"/envs")
}
// ─── Databases ────────────────────────────────────────────────────────────────
func CoolifyListDatabases(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/databases")
}
func CoolifyGetDatabase(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/databases/"+c.Params("uuid"))
}
func CoolifyDatabaseStart(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/databases/"+c.Params("uuid")+"/start")
}
func CoolifyDatabaseStop(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/databases/"+c.Params("uuid")+"/stop")
}
func CoolifyDatabaseRestart(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/databases/"+c.Params("uuid")+"/restart")
}
// ─── Projects ─────────────────────────────────────────────────────────────────
func CoolifyListProjects(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/projects")
}
func CoolifyGetProject(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/projects/"+c.Params("uuid"))
}
func CoolifyProjectEnvironments(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/projects/"+c.Params("uuid")+"/environments")
}
// ─── Deployments ──────────────────────────────────────────────────────────────
func CoolifyListDeployments(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/deployments")
}
func CoolifyGetDeployment(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/deployments/"+c.Params("uuid"))
}
// ─── Teams ────────────────────────────────────────────────────────────────────
func CoolifyListTeams(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/teams")
}
func CoolifyCurrentTeam(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/teams/current")
}
func CoolifyTeamMembers(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/teams/current/members")
}
// CoolifyWebhook recibe notificaciones push de Coolify (deployments, etc.)
// Acepta el payload y devuelve 200; el procesamiento puede extenderse aquí.
func CoolifyWebhook(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
// ─── CRUD de instancias Coolify ───────────────────────────────────────────────
func CoolifyListConfigs(c *fiber.Ctx) error {
items, err := models.GetAllCoolifyConfigs()
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
// Ocultar tokens
type safe struct {
ID uint `json:"id"`
Nombre string `json:"nombre"`
BaseURL string `json:"base_url"`
Activo bool `json:"activo"`
}
out := make([]safe, len(items))
for i, cfg := range items {
out[i] = safe{ID: cfg.ID, Nombre: cfg.Nombre, BaseURL: cfg.BaseURL, Activo: cfg.Activo}
}
return c.JSON(fiber.Map{"items": out})
}
func CoolifyCreateConfig(c *fiber.Ctx) error {
type Req struct {
Nombre string `json:"nombre"`
BaseURL string `json:"base_url"`
ApiToken string `json:"api_token"`
Activo bool `json:"activo"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "body inválido"})
}
req.BaseURL = strings.TrimRight(strings.TrimSpace(req.BaseURL), "/")
if req.BaseURL == "" || req.ApiToken == "" {
return c.Status(400).JSON(fiber.Map{"error": "base_url y api_token son requeridos"})
}
cfg := &models.CoolifyConfig{Nombre: req.Nombre, BaseURL: req.BaseURL, ApiToken: req.ApiToken, Activo: req.Activo}
if err := models.CreateCoolifyConfig(cfg); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(fiber.Map{"ok": true, "id": cfg.ID})
}
func CoolifyUpdateConfig(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"})
}
type Req struct {
Nombre string `json:"nombre"`
BaseURL string `json:"base_url"`
ApiToken string `json:"api_token"`
Activo bool `json:"activo"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "body inválido"})
}
req.BaseURL = strings.TrimRight(strings.TrimSpace(req.BaseURL), "/")
cfg, err := models.UpdateCoolifyConfig(uint(id), req.Nombre, req.BaseURL, req.ApiToken, req.Activo)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true, "id": cfg.ID})
}
func CoolifyDeleteConfig(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"})
}
if err := models.DeleteCoolifyConfig(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
func CoolifyTestConfig(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"})
}
cfg, err := models.GetCoolifyConfigByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "config no encontrada"})
}
base := strings.TrimRight(cfg.BaseURL, "/")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(base + "/api/health")
if err != nil {
return c.Status(502).JSON(fiber.Map{"error": err.Error()})
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
var result json.RawMessage
if jsonErr := json.Unmarshal(body, &result); jsonErr != nil {
result = json.RawMessage(fmt.Sprintf(`{"raw":%q}`, string(body)))
}
c.Status(resp.StatusCode)
return c.JSON(result)
}