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:
2026-04-05 01:27:54 +03:00
parent 1aa9c3f0e9
commit 187e302f4a
18 changed files with 525 additions and 63 deletions
+84 -5
View File
@@ -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