From 057b2e543988d8b3b6a9c86fb56336299c7111b4 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 18:51:42 +0200 Subject: [PATCH] fix(api/v2): publish OpenAPI Servers and make schemas publicly fetchable Huma's SchemaLinkTransformer (enabled by default) emits a `$schema` field on every JSON response and an example URL in the spec. Both were broken in our setup: the example URL used Huma's "https://example.com" placeholder because no Servers were declared, and the runtime URL pointed at /schemas/Label.json instead of /api/v2/schemas/Label.json because Huma can't see the Echo group prefix. Two changes: - Set OpenAPI Servers to a list with the relative GroupPrefix first and, if service.publicurl is configured, the absolute deployment URL second. Servers[0] feeds Huma's getAPIPrefix / addSchemaField / Transform fallback; Servers[1] is informational metadata for SDK generators and docs UIs. Keeping the relative URL at index 0 dodges a Huma quirk that double-prefixes the runtime $schema URL when the index-0 server URL carries a path component. - Add /api/v2/schemas/:schema to unauthenticatedAPIPaths so editors and SDK tooling can fetch schemas without a token, mirroring how the spec itself is reachable. --- pkg/routes/api/v2/huma.go | 16 ++++++++++++++++ pkg/routes/routes.go | 1 + 2 files changed, 17 insertions(+) diff --git a/pkg/routes/api/v2/huma.go b/pkg/routes/api/v2/huma.go index 9d2e44556..720a5929d 100644 --- a/pkg/routes/api/v2/huma.go +++ b/pkg/routes/api/v2/huma.go @@ -20,7 +20,9 @@ package apiv2 import ( "context" "net/http" + "strings" + "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/modules/humaecho5" "code.vikunja.io/api/pkg/version" @@ -65,6 +67,20 @@ func NewAPI(e *echo.Echo, g *echo.Group) huma.API { {"JWTKeyAuth": {}}, {"APITokenAuth": {}}, } + // The relative entry MUST stay at index 0. Huma's SchemaLinkTransformer + // reads Servers[0] in three places (getAPIPrefix, addSchemaField, the + // runtime Transform fallback). With a path-bearing absolute URL at + // index 0, the runtime fallback concatenates that URL onto a ref that + // already includes /api/v2, producing a double-prefixed $schema link + // like https://host/api/v2/api/v2/schemas/Label.json. A relative URL + // at index 0 keeps the prefix in the transformer's bookkeeping while + // the absolute URL at index 1 advertises the deployment URL to SDK + // generators and docs UIs. + servers := []*huma.Server{{URL: GroupPrefix}} + if publicURL := strings.TrimRight(config.ServicePublicURL.GetString(), "/"); publicURL != "" { + servers = append(servers, &huma.Server{URL: publicURL + GroupPrefix}) + } + oapi.Servers = servers return api } diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index ceb82d20b..c81dcef1f 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -342,6 +342,7 @@ var unauthenticatedAPIPaths = map[string]bool{ "/api/v2/openapi-3.0.yaml": true, "/api/v2/docs": true, "/api/v2/docs/scalar.standalone.js": true, + "/api/v2/schemas/:schema": true, } // collectRoutesForAPITokens collects all routes for API token permission checking.