feat: Publish an OpenAPI 3.1 document and a Scalar reference
Generated from swaggo v2 annotations, committed rather than built into the image: the runtime stage is scratch and adding codegen puts the toolchain in the build. CI regenerates and diffs, so an annotation edited without regenerating fails the build — without that the annotations would drift while still looking authoritative. Scalar is vendored rather than loaded from a CDN, because air-gapped self-hosted installs are supported and a reference page that fails closed offline is a support ticket.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
// Package docs holds the generated OpenAPI document and the vendored Scalar
|
||||
// bundle that renders it.
|
||||
//
|
||||
// openapi.json is generated by `swag init` and committed rather than built into
|
||||
// the image: server/Dockerfile produces a scratch runtime from a Go build
|
||||
// stage, and adding codegen there means putting the toolchain in the image.
|
||||
// server-deploy.yml regenerates and diffs it, so an annotation edited without
|
||||
// regenerating fails the build.
|
||||
//
|
||||
// scalar.standalone.js is vendored from
|
||||
// https://cdn.jsdelivr.net/npm/@scalar/api-reference@latest/dist/browser/standalone.js
|
||||
// and refreshed by hand. Fetched at build time it would break an air-gapped
|
||||
// install; fetched at page load it would break an air-gapped install more
|
||||
// visibly.
|
||||
package docs
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed openapi.json
|
||||
var OpenAPI []byte
|
||||
|
||||
//go:embed scalar.standalone.js
|
||||
var ScalarJS []byte
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/api/docs"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// scalarPage renders the reference against this instance's own spec, so "Try
|
||||
// it" acts on the reader's API with the reader's session.
|
||||
const scalarPage = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Vantage API</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="/api/docs/scalar.js"></script>
|
||||
<script>
|
||||
Scalar.createApiReference('#app', {
|
||||
url: '/api/openapi.json',
|
||||
theme: 'deepSpace',
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// getOpenAPI godoc
|
||||
//
|
||||
// @Summary Get the OpenAPI document
|
||||
// @Description Generated from swaggo annotations at build time and committed; served verbatim.
|
||||
// @Tags docs
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security cookieAuth
|
||||
// @Security bearerAuth
|
||||
// @Router /openapi.json [get]
|
||||
func getOpenAPI(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "application/json; charset=utf-8", docs.OpenAPI)
|
||||
}
|
||||
|
||||
// getScalarJS godoc
|
||||
//
|
||||
// @Summary Get the vendored Scalar bundle
|
||||
// @Description Served locally rather than from a CDN so the reference page works on an air-gapped install.
|
||||
// @Tags docs
|
||||
// @Produce application/javascript
|
||||
// @Success 200 {string} string "javascript bundle"
|
||||
// @Security cookieAuth
|
||||
// @Security bearerAuth
|
||||
// @Router /docs/scalar.js [get]
|
||||
func getScalarJS(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "application/javascript; charset=utf-8", docs.ScalarJS)
|
||||
}
|
||||
|
||||
// getAPIDocs godoc
|
||||
//
|
||||
// @Summary API reference page
|
||||
// @Description Renders the Scalar reference against this instance's own OpenAPI document.
|
||||
// @Tags docs
|
||||
// @Produce html
|
||||
// @Success 200 {string} string "HTML page"
|
||||
// @Security cookieAuth
|
||||
// @Security bearerAuth
|
||||
// @Router /docs [get]
|
||||
func getAPIDocs(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(scalarPage))
|
||||
}
|
||||
@@ -148,6 +148,13 @@ var routeScopes = map[string]string{
|
||||
"GET /api/tokens/scopes": "settings:read",
|
||||
"POST /api/tokens": "settings:write",
|
||||
"DELETE /api/tokens/:id": "settings:write",
|
||||
|
||||
// The generated OpenAPI document and its Scalar reference page. Read-only,
|
||||
// so they share the settings:read scope with the rest of the docs a token
|
||||
// can already see about its own instance.
|
||||
"GET /api/openapi.json": "settings:read",
|
||||
"GET /api/docs": "settings:read",
|
||||
"GET /api/docs/scalar.js": "settings:read",
|
||||
}
|
||||
|
||||
// RequireScopes enforces routeScopes for token-authenticated requests and does
|
||||
|
||||
Reference in New Issue
Block a user