feat: proxy routes page, OIDC login fix, NPM test connection, webhook URL fix, and UX improvements
- Add /proxies page showing deploy-managed proxy routes with project/stage links, search, and status - Add GET /api/proxies endpoint joining instances with project/stage names - Add POST /api/settings/npm/test endpoint for NPM connection validation - Add GET /api/auth/mode public endpoint for auth mode detection - Add NPM Test Connection button with validation on save - Fix OIDC SSO button only shown when auth_mode is oidc - Fix webhook URL showing empty when domain not set (fallback to request host) - Fix quick deploy double-tag (image:latest:latest) by splitting tag from image URL - Fix trim() errors on number inputs in deploy and settings forms - Fix NPM client auto-append /api to base URL - Sanitize NPM test error messages (no raw HTML) - Remove healthcheck field from Quick Deploy form - Fix env vars placeholder newline - Make domain field optional in settings - Set polling interval minimum to 60s - Add Proxies and Events to sidebar navigation - Fix SSL cert name flash on NPM settings page - Fix empty state icon on proxies page
This commit is contained in:
@@ -258,8 +258,15 @@ func (s *Server) getWebhookURL(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
webhookURL := ""
|
||||
if settings.WebhookSecret != "" && settings.Domain != "" {
|
||||
webhookURL = fmt.Sprintf("https://%s/api/webhook/%s", settings.Domain, settings.WebhookSecret)
|
||||
if settings.WebhookSecret != "" {
|
||||
host := settings.Domain
|
||||
scheme := "https"
|
||||
if host == "" {
|
||||
// Fall back to request host for dev/local setups.
|
||||
host = r.Host
|
||||
scheme = "http"
|
||||
}
|
||||
webhookURL = fmt.Sprintf("%s://%s/api/webhook/%s", scheme, host, settings.WebhookSecret)
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
@@ -281,10 +288,13 @@ func (s *Server) regenerateWebhookSecret(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
webhookURL := ""
|
||||
if settings.Domain != "" {
|
||||
webhookURL = fmt.Sprintf("https://%s/api/webhook/%s", settings.Domain, secret)
|
||||
host := settings.Domain
|
||||
scheme := "https"
|
||||
if host == "" {
|
||||
host = r.Host
|
||||
scheme = "http"
|
||||
}
|
||||
webhookURL := fmt.Sprintf("%s://%s/api/webhook/%s", scheme, host, secret)
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"webhook_url": webhookURL,
|
||||
@@ -504,6 +514,75 @@ type dnsTestRequest struct {
|
||||
ZoneID string `json:"zone_id"`
|
||||
}
|
||||
|
||||
// testNpmConnection handles POST /api/settings/npm/test.
|
||||
// Tests connectivity and authentication to the NPM API.
|
||||
func (s *Server) testNpmConnection(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
URL string `json:"npm_url"`
|
||||
Email string `json:"npm_email"`
|
||||
Password string `json:"npm_password"`
|
||||
}
|
||||
if !decodeJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
// Use provided values, fall back to stored settings.
|
||||
settings, err := s.store.GetSettings()
|
||||
if err != nil {
|
||||
slog.Error("failed to get settings", "error", err)
|
||||
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
npmURL := req.URL
|
||||
if npmURL == "" {
|
||||
npmURL = settings.NpmURL
|
||||
}
|
||||
if npmURL == "" {
|
||||
respondError(w, http.StatusBadRequest, "NPM URL is required")
|
||||
return
|
||||
}
|
||||
|
||||
email := req.Email
|
||||
if email == "" {
|
||||
email = settings.NpmEmail
|
||||
}
|
||||
|
||||
password := req.Password
|
||||
if password == "" && settings.NpmPassword != "" {
|
||||
decrypted, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "failed to decrypt stored NPM password")
|
||||
return
|
||||
}
|
||||
password = decrypted
|
||||
}
|
||||
|
||||
if email == "" || password == "" {
|
||||
respondError(w, http.StatusBadRequest, "NPM email and password are required")
|
||||
return
|
||||
}
|
||||
|
||||
// Test connectivity.
|
||||
client := npm.New(npmURL)
|
||||
ctx := r.Context()
|
||||
|
||||
if err := client.Ping(ctx); err != nil {
|
||||
slog.Warn("npm test: ping failed", "url", npmURL, "error", err)
|
||||
respondError(w, http.StatusBadGateway, "Cannot reach NPM at "+npmURL)
|
||||
return
|
||||
}
|
||||
|
||||
// Test authentication.
|
||||
if err := client.Authenticate(ctx, email, password); err != nil {
|
||||
slog.Warn("npm test: auth failed", "url", npmURL, "error", err)
|
||||
respondError(w, http.StatusBadGateway, "NPM authentication failed — check email and password")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]string{"status": "connected"})
|
||||
}
|
||||
|
||||
// testDNSConnection handles POST /api/settings/dns/test.
|
||||
func (s *Server) testDNSConnection(w http.ResponseWriter, r *http.Request) {
|
||||
var req dnsTestRequest
|
||||
|
||||
Reference in New Issue
Block a user