package api import ( "fmt" "net/http" "strings" "github.com/alexei/docker-watcher/internal/crypto" "github.com/alexei/docker-watcher/internal/npm" "github.com/alexei/docker-watcher/internal/webhook" ) // settingsRequest is the expected JSON body for updating settings. type settingsRequest struct { Domain string `json:"domain"` ServerIP string `json:"server_ip"` Network string `json:"network"` SubdomainPattern string `json:"subdomain_pattern"` NotificationURL string `json:"notification_url"` NpmURL string `json:"npm_url"` NpmEmail string `json:"npm_email"` NpmPassword string `json:"npm_password"` PollingInterval string `json:"polling_interval"` SSLCertificateID *int `json:"ssl_certificate_id,omitempty"` } // getSettings handles GET /api/settings. func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) { settings, err := s.store.GetSettings() if err != nil { respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error()) return } // Return settings without sensitive fields. respondJSON(w, http.StatusOK, map[string]any{ "domain": settings.Domain, "server_ip": settings.ServerIP, "network": settings.Network, "subdomain_pattern": settings.SubdomainPattern, "notification_url": settings.NotificationURL, "npm_url": settings.NpmURL, "npm_email": settings.NpmEmail, "has_npm_password": settings.NpmPassword != "", "polling_interval": settings.PollingInterval, "ssl_certificate_id": settings.SSLCertificateID, "updated_at": settings.UpdatedAt, }) } // updateSettings handles PUT /api/settings. func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) { var req settingsRequest if !decodeJSON(w, r, &req) { return } existing, err := s.store.GetSettings() if err != nil { respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error()) return } updated := existing if req.Domain != "" { updated.Domain = req.Domain } if req.ServerIP != "" { updated.ServerIP = req.ServerIP } if req.Network != "" { updated.Network = req.Network } if req.SubdomainPattern != "" { updated.SubdomainPattern = req.SubdomainPattern } // Allow clearing notification URL. updated.NotificationURL = req.NotificationURL if req.NpmURL != "" { updated.NpmURL = req.NpmURL } if req.NpmEmail != "" { updated.NpmEmail = req.NpmEmail } if req.NpmPassword != "" { encPassword, err := crypto.Encrypt(s.encKey, req.NpmPassword) if err != nil { respondError(w, http.StatusInternalServerError, "failed to encrypt npm password: "+err.Error()) return } updated.NpmPassword = encPassword } if req.PollingInterval != "" { updated.PollingInterval = req.PollingInterval } if req.SSLCertificateID != nil { updated.SSLCertificateID = *req.SSLCertificateID } if err := s.store.UpdateSettings(updated); err != nil { respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error()) return } respondJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } // getWebhookURL handles GET /api/settings/webhook-url. func (s *Server) getWebhookURL(w http.ResponseWriter, r *http.Request) { settings, err := s.store.GetSettings() if err != nil { respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error()) return } webhookURL := "" if settings.WebhookSecret != "" && settings.Domain != "" { webhookURL = fmt.Sprintf("https://%s/api/webhook/%s", settings.Domain, settings.WebhookSecret) } respondJSON(w, http.StatusOK, map[string]string{ "webhook_url": webhookURL, }) } // regenerateWebhookSecret handles POST /api/settings/regenerate. func (s *Server) regenerateWebhookSecret(w http.ResponseWriter, r *http.Request) { secret, err := webhook.RegenerateWebhookSecret(s.store) if err != nil { respondError(w, http.StatusInternalServerError, "failed to regenerate webhook secret: "+err.Error()) return } settings, err := s.store.GetSettings() if err != nil { respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error()) return } webhookURL := "" if settings.Domain != "" { webhookURL = fmt.Sprintf("https://%s/api/webhook/%s", settings.Domain, secret) } respondJSON(w, http.StatusOK, map[string]string{ "webhook_url": webhookURL, "webhook_secret": secret, }) } // listNpmCertificates handles GET /api/settings/npm-certificates. // It authenticates to NPM using the stored credentials and returns only wildcard certificates. func (s *Server) listNpmCertificates(w http.ResponseWriter, r *http.Request) { settings, err := s.store.GetSettings() if err != nil { respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error()) return } if settings.NpmURL == "" || settings.NpmEmail == "" || settings.NpmPassword == "" { respondError(w, http.StatusBadRequest, "NPM credentials not configured") return } npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword) if err != nil { respondError(w, http.StatusInternalServerError, "failed to decrypt npm password: "+err.Error()) return } client := npm.New(settings.NpmURL) if err := client.Authenticate(r.Context(), settings.NpmEmail, npmPassword); err != nil { respondError(w, http.StatusBadGateway, "failed to authenticate to NPM: "+err.Error()) return } certs, err := client.ListCertificates(r.Context()) if err != nil { respondError(w, http.StatusBadGateway, "failed to list certificates: "+err.Error()) return } // Filter to wildcard certificates only. var wildcards []npm.Certificate for _, cert := range certs { if isWildcardCert(cert) { wildcards = append(wildcards, cert) } } if wildcards == nil { wildcards = []npm.Certificate{} } respondJSON(w, http.StatusOK, wildcards) } // isWildcardCert returns true if any of the certificate's domain names contains "*". func isWildcardCert(cert npm.Certificate) bool { for _, d := range cert.DomainNames { if strings.Contains(d, "*") { return true } } return false }