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.
This commit is contained in:
kolaente 2026-05-30 18:51:42 +02:00 committed by kolaente
parent 00b42234e9
commit 057b2e5439
2 changed files with 17 additions and 0 deletions

View File

@ -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
}

View File

@ -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.