Pārlūkot izejas kodu

fix(api-docs): target the panel base path in OpenAPI servers

ServeOpenAPISpec shipped servers:[{url:"/"}], so Swagger UI "Try it out" and external generators hit the origin root and ignored a non-root webBasePath. Inject the runtime base path into the single servers entry at serve time, touching only that field via json.RawMessage so the rest of the spec is preserved verbatim.
MHSanaei 1 dienu atpakaļ
vecāks
revīzija
e56f6c63f6
2 mainītis faili ar 75 papildinājumiem un 0 dzēšanām
  1. 33 0
      web/controller/dist.go
  2. 42 0
      web/controller/dist_test.go

+ 33 - 0
web/controller/dist.go

@@ -3,6 +3,7 @@ package controller
 import (
 	"bytes"
 	"embed"
+	"encoding/json"
 	htmlpkg "html"
 	"net/http"
 	"strings"
@@ -34,10 +35,42 @@ func ServeOpenAPISpec(c *gin.Context) {
 		c.JSON(http.StatusNotFound, gin.H{"success": false, "msg": "openapi.json not found"})
 		return
 	}
+
+	// The embedded spec ships with `servers: [{url: "/"}]`. When the panel runs
+	// under a non-root web base path, Swagger UI "Try it out" and external
+	// generators must target that prefix, so rewrite the single server entry to
+	// the runtime base path before serving.
+	if basePath := c.GetString("base_path"); basePath != "" && basePath != "/" {
+		if rebuilt, err := withServerBasePath(body, basePath); err != nil {
+			logger.Warning("openapi.json: could not inject base path:", err)
+		} else {
+			body = rebuilt
+		}
+	}
+
 	c.Header("Cache-Control", "public, max-age=300")
 	c.Data(http.StatusOK, "application/json; charset=utf-8", body)
 }
 
+// withServerBasePath rewrites the spec's `servers` entry so requests target the
+// panel's configured web base path. Only the top-level `servers` field is
+// replaced; every other field is preserved verbatim via json.RawMessage.
+func withServerBasePath(spec []byte, basePath string) ([]byte, error) {
+	var doc map[string]json.RawMessage
+	if err := json.Unmarshal(spec, &doc); err != nil {
+		return nil, err
+	}
+	servers, err := json.Marshal([]map[string]string{{
+		"url":         strings.TrimSuffix(basePath, "/"),
+		"description": "Current panel",
+	}})
+	if err != nil {
+		return nil, err
+	}
+	doc["servers"] = servers
+	return json.Marshal(doc)
+}
+
 func serveDistPage(c *gin.Context, name string) {
 	body, err := distFS.ReadFile("dist/" + name)
 	if err != nil {

+ 42 - 0
web/controller/dist_test.go

@@ -0,0 +1,42 @@
+package controller
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+func TestWithServerBasePath(t *testing.T) {
+	spec := []byte(`{"openapi":"3.0.3","info":{"title":"x"},"servers":[{"url":"/","description":"old"}],"paths":{"/p":{"get":{"summary":"s"}}}}`)
+
+	out, err := withServerBasePath(spec, "/test/")
+	if err != nil {
+		t.Fatalf("withServerBasePath: %v", err)
+	}
+
+	var doc map[string]any
+	if err := json.Unmarshal(out, &doc); err != nil {
+		t.Fatalf("unmarshal result: %v", err)
+	}
+
+	servers, ok := doc["servers"].([]any)
+	if !ok || len(servers) != 1 {
+		t.Fatalf("servers = %v, want one entry", doc["servers"])
+	}
+	srv, _ := servers[0].(map[string]any)
+	if srv["url"] != "/test" {
+		t.Errorf("server url = %v, want /test (trailing slash trimmed)", srv["url"])
+	}
+
+	if doc["openapi"] != "3.0.3" {
+		t.Errorf("openapi field not preserved: %v", doc["openapi"])
+	}
+	if _, ok := doc["paths"].(map[string]any)["/p"]; !ok {
+		t.Errorf("paths content not preserved verbatim")
+	}
+}
+
+func TestWithServerBasePathInvalidJSON(t *testing.T) {
+	if _, err := withServerBasePath([]byte("not json"), "/test/"); err == nil {
+		t.Errorf("expected error on invalid spec, got nil")
+	}
+}