feat: app-to-app integration framework + OnlyOffice handlers
Generic integration system for connecting deployed apps via toggle UI. First handlers: OnlyOffice→FileBrowser (config.yaml patch) and OnlyOffice→Nextcloud (occ CLI). Lifecycle hooks auto-suspend on stop and re-apply on start. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/assets"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/integrations"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
cf "gitea.dooplex.hu/admin/felhom-controller/internal/cloudflare"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
@@ -55,6 +56,9 @@ type Router struct {
|
||||
|
||||
// Geo-restriction sync manager
|
||||
geoSync *cf.GeoSyncManager
|
||||
|
||||
// App-to-app integration manager (nil if not configured)
|
||||
integrationMgr *integrations.Manager
|
||||
}
|
||||
|
||||
// SetAssetsSyncer sets the Hub asset syncer for on-demand sync triggers.
|
||||
@@ -67,6 +71,11 @@ func (r *Router) SetGeoSync(gs *cf.GeoSyncManager) {
|
||||
r.geoSync = gs
|
||||
}
|
||||
|
||||
// SetIntegrationManager sets the app-to-app integration manager.
|
||||
func (r *Router) SetIntegrationManager(im *integrations.Manager) {
|
||||
r.integrationMgr = im
|
||||
}
|
||||
|
||||
func NewRouter(cfg *config.Config, configPath string, sett *settings.Settings, stackMgr *stacks.Manager, syncer *catalogsync.Syncer, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, crossDrive *backup.CrossDriveRunner, metricsStore *metrics.MetricsStore, updater *selfupdate.Updater, notif *notify.Notifier, logger *log.Logger) *Router {
|
||||
return &Router{cfg: cfg, configPath: configPath, sett: sett, stackMgr: stackMgr, syncer: syncer, cpuCollector: cpuCollector, backupMgr: backupMgr, crossDriveRunner: crossDrive, metricsStore: metricsStore, updater: updater, notifier: notif, logger: logger}
|
||||
}
|
||||
@@ -120,6 +129,23 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
case path == "/config" && req.Method == http.MethodGet:
|
||||
r.configContent(w, req)
|
||||
|
||||
// --- Integration routes (must be before hasSuffix-based stack cases) ---
|
||||
|
||||
// GET /api/integrations/{provider} — list integrations for a provider
|
||||
case strings.HasPrefix(path, "/integrations/") && !strings.Contains(strings.TrimPrefix(path, "/integrations/"), "/") && req.Method == http.MethodGet:
|
||||
provider := strings.TrimPrefix(path, "/integrations/")
|
||||
r.listIntegrations(w, provider)
|
||||
|
||||
// POST /api/integrations/{provider}/{target} — toggle integration
|
||||
case strings.HasPrefix(path, "/integrations/") && req.Method == http.MethodPost:
|
||||
rest := strings.TrimPrefix(path, "/integrations/")
|
||||
parts := strings.SplitN(rest, "/", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid integration path"})
|
||||
return
|
||||
}
|
||||
r.toggleIntegration(w, req, parts[0], parts[1])
|
||||
|
||||
// GET /api/stacks/{name}/deploy-fields
|
||||
case hasSuffix(path, "/deploy-fields") && req.Method == http.MethodGet:
|
||||
r.getDeployFields(w, req, extractName(path, "/deploy-fields"))
|
||||
@@ -361,6 +387,11 @@ func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name stri
|
||||
if r.OnGeoRelevantChange != nil {
|
||||
go r.OnGeoRelevantChange()
|
||||
}
|
||||
|
||||
// Re-apply integrations that target this newly deployed stack
|
||||
if r.integrationMgr != nil {
|
||||
go r.integrationMgr.OnStackStart(context.Background(), name)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
|
||||
@@ -419,6 +450,16 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Stack " + name + " " + action + " completed"})
|
||||
|
||||
// Trigger integration lifecycle hooks after successful action
|
||||
if r.integrationMgr != nil {
|
||||
switch action {
|
||||
case "start", "restart":
|
||||
go r.integrationMgr.OnStackStart(context.Background(), name)
|
||||
case "stop":
|
||||
go r.integrationMgr.OnStackStop(context.Background(), name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) updateOptionalConfig(w http.ResponseWriter, req *http.Request, name string) {
|
||||
@@ -442,6 +483,55 @@ func (r *Router) updateOptionalConfig(w http.ResponseWriter, req *http.Request,
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Beállítások frissítve"})
|
||||
}
|
||||
|
||||
// --- Integration API handlers ---
|
||||
|
||||
func (r *Router) listIntegrations(w http.ResponseWriter, provider string) {
|
||||
if r.integrationMgr == nil {
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: []integrations.StatusInfo{}})
|
||||
return
|
||||
}
|
||||
list := r.integrationMgr.ListForProvider(provider)
|
||||
if list == nil {
|
||||
list = []integrations.StatusInfo{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: list})
|
||||
}
|
||||
|
||||
func (r *Router) toggleIntegration(w http.ResponseWriter, req *http.Request, provider, target string) {
|
||||
limitBody(w, req)
|
||||
if r.integrationMgr == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, apiResponse{OK: false, Error: "integrations not available"})
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
action := "enable"
|
||||
if !body.Enabled {
|
||||
action = "disable"
|
||||
}
|
||||
r.logger.Printf("[API] Integration %s requested: %s:%s", action, provider, target)
|
||||
|
||||
state, err := r.integrationMgr.Toggle(req.Context(), provider, target, body.Enabled)
|
||||
if err != nil {
|
||||
r.logger.Printf("[API] Integration toggle failed for %s:%s: %v", provider, target, err)
|
||||
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
msg := "Integráció engedélyezve"
|
||||
if !body.Enabled {
|
||||
msg = "Integráció kikapcsolva"
|
||||
}
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: state, Message: msg})
|
||||
}
|
||||
|
||||
func (r *Router) getStackLogs(w http.ResponseWriter, req *http.Request, name string) {
|
||||
lines := 100
|
||||
if v := req.URL.Query().Get("lines"); v != "" {
|
||||
@@ -550,6 +640,11 @@ func (r *Router) removeStack(w http.ResponseWriter, req *http.Request, name stri
|
||||
r.notifier.NotifyAppRemoved(name, name)
|
||||
}
|
||||
|
||||
// Clean up integrations for removed stack
|
||||
if r.integrationMgr != nil {
|
||||
r.integrationMgr.OnStackRemove(context.Background(), name)
|
||||
}
|
||||
|
||||
// Re-sync geo rules (hostname removed)
|
||||
if r.OnGeoRelevantChange != nil {
|
||||
go r.OnGeoRelevantChange()
|
||||
|
||||
Reference in New Issue
Block a user