Просмотр исходного кода

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 день назад
Родитель
Сommit
e56f6c63f6
2 измененных файлов с 75 добавлено и 0 удалено
  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")
+	}
+}