package integrations import ( "context" "fmt" "log" "path/filepath" "sync" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/crypto" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // StackProvider abstracts stacks.Manager to break circular imports. type StackProvider interface { GetStack(name string) (*stacks.Stack, bool) GetStacks() []stacks.Stack RestartStack(name string) error } // Manager coordinates integration apply/revoke and lifecycle hooks. type Manager struct { sett *settings.Settings stacks StackProvider domain string stacksDir string encKey []byte logger *log.Logger handlers map[string]Handler // key: "provider:target" -> Handler mu sync.Mutex // serialize apply/revoke operations } // NewManager creates an integration manager and registers built-in handlers. func NewManager(sett *settings.Settings, sp StackProvider, domain, stacksDir string, encKey []byte, logger *log.Logger) *Manager { m := &Manager{ sett: sett, stacks: sp, domain: domain, stacksDir: stacksDir, encKey: encKey, logger: logger, handlers: make(map[string]Handler), } // Register built-in handlers m.RegisterHandler("onlyoffice:filebrowser", &OnlyOfficeFileBrowserHandler{}) m.RegisterHandler("onlyoffice:nextcloud", &OnlyOfficeNextcloudHandler{}) return m } // RegisterHandler registers a handler for a provider:target integration key. func (m *Manager) RegisterHandler(key string, h Handler) { m.handlers[key] = h } // Toggle enables or disables an integration. func (m *Manager) Toggle(ctx context.Context, provider, target string, enable bool) (settings.IntegrationState, error) { m.mu.Lock() defer m.mu.Unlock() key := IntegrationKey(provider, target) handler, ok := m.handlers[key] if !ok { return settings.IntegrationState{}, fmt.Errorf("nincs kezelő a(z) %s integrációhoz", key) } ac, err := m.buildApplyContext(provider, target) if err != nil { return settings.IntegrationState{}, err } var state settings.IntegrationState if enable { // Validate: provider must be deployed and running provStack, pOk := m.stacks.GetStack(provider) if !pOk || !provStack.Deployed { return state, fmt.Errorf("a szolgáltató alkalmazás (%s) nincs telepítve", provider) } if provStack.State != stacks.StateRunning { return state, fmt.Errorf("a szolgáltató alkalmazás (%s) nem fut", provider) } // Validate: target must be deployed and running (filebrowser is infra, always present) if target != "filebrowser" { tgtStack, tOk := m.stacks.GetStack(target) if !tOk || !tgtStack.Deployed { return state, fmt.Errorf("a célalkalmazás (%s) nincs telepítve", target) } if tgtStack.State != stacks.StateRunning { return state, fmt.Errorf("a célalkalmazás (%s) nem fut", target) } } if err := handler.Apply(ac); err != nil { state.Enabled = true state.Status = "error" state.LastError = err.Error() state.EnabledAt = time.Now().UTC().Format(time.RFC3339) _ = m.sett.SetIntegrationState(key, state) return state, fmt.Errorf("integráció alkalmazása sikertelen: %w", err) } state.Enabled = true state.Status = "active" state.EnabledAt = time.Now().UTC().Format(time.RFC3339) m.logger.Printf("[INFO] Integration %s enabled", key) } else { if err := handler.Revoke(ac); err != nil { m.logger.Printf("[WARN] Integration revoke failed for %s: %v", key, err) state.LastError = err.Error() } state.Enabled = false state.Status = "disabled" m.logger.Printf("[INFO] Integration %s disabled", key) } if err := m.sett.SetIntegrationState(key, state); err != nil { return state, fmt.Errorf("integráció állapotának mentése sikertelen: %w", err) } return state, nil } // ListForProvider returns StatusInfo for all integrations defined in a provider's metadata. func (m *Manager) ListForProvider(providerSlug string) []StatusInfo { provStack, ok := m.stacks.GetStack(providerSlug) if !ok { return nil } var result []StatusInfo for _, idef := range provStack.Meta.Integrations { key := IntegrationKey(providerSlug, idef.Target) state, _ := m.sett.GetIntegrationState(key) targetDeployed := false targetRunning := false if tgt, tOk := m.stacks.GetStack(idef.Target); tOk { // FileBrowser is infrastructure — if its stack exists, it's "deployed" if idef.Target == "filebrowser" { targetDeployed = true } else { targetDeployed = tgt.Deployed } targetRunning = tgt.State == stacks.StateRunning } result = append(result, StatusInfo{ Key: key, Provider: providerSlug, Target: idef.Target, Label: idef.Label, Description: idef.Description, Enabled: state.Enabled, Status: state.Status, LastError: state.LastError, TargetDeployed: targetDeployed, TargetRunning: targetRunning, }) } return result } // buildApplyContext gathers all data needed by a handler. func (m *Manager) buildApplyContext(provider, target string) (*ApplyContext, error) { provStack, ok := m.stacks.GetStack(provider) if !ok { return nil, fmt.Errorf("szolgáltató stack %q nem található", provider) } // Load decrypted env from provider's app.yaml provEnv := m.loadDecryptedEnv(provStack) provMeta := provStack.Meta return &ApplyContext{ ProviderName: provider, TargetName: target, Domain: m.domain, ProviderEnv: provEnv, ProviderMeta: &provMeta, StacksDir: m.stacksDir, Logger: m.logger, RestartStack: m.stacks.RestartStack, }, nil } // loadDecryptedEnv reads app.yaml for a stack and decrypts sensitive values. func (m *Manager) loadDecryptedEnv(s *stacks.Stack) map[string]string { stackDir := filepath.Dir(s.ComposePath) cfg := stacks.LoadAppConfig(stackDir) if cfg == nil { return nil } if m.encKey != nil { cfg.Env = crypto.DecryptMap(m.encKey, cfg.Env) } return cfg.Env }