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) }