e0b002975f
The OpenAPI spec and Swagger UI were public. Put them behind the same authentication as the rest of the API, and add a compact route list so a client can ask "what can I call?" without opening dev tools. Access: - /api/openapi.json and /api/routes require a bearer token or API key. - /api/docs additionally accepts a session cookie set at login, so a signed-in operator can open the docs in a new tab; an anonymous browser is redirected to /login?redirect=... and returned afterwards. - The cookie is HttpOnly and path-scoped to /api/docs, so it is never sent to /api/v1/* and cannot authenticate an API call (no CSRF surface). Verified: cookie-only request to /api/v1/rules returns 401. Discovery: both the spec and GET /api/routes accept ?method=get,post and ?path=<substring> (comma-separated, case-insensitive). The route list returns method, path, summary, tag, public, and `allowed` — false when a read-scoped API key cannot invoke that route. /api/docs passes the same query through to the spec it loads. UI: a </> icon in the header (both layouts) and an Administration → API Docs menu entry, opened in a new tab via a new `external` nav-item flag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
52 lines
1.6 KiB
Go
52 lines
1.6 KiB
Go
// Package api - docs session cookie.
|
|
//
|
|
// The SPA keeps its session token in localStorage and sends it as a bearer
|
|
// header, so a plain browser navigation to /api/docs carries no credentials.
|
|
// To let a signed-in operator open the interactive docs in a new tab, login
|
|
// also sets the session token as an HttpOnly cookie scoped to Path=/api/docs.
|
|
// Because of the path scope the cookie is never sent to any /api/v1 route, so
|
|
// cookie auth cannot be used for state-changing requests (no CSRF surface);
|
|
// Swagger's "Try it out" still needs an explicit bearer token / API key.
|
|
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// DocsCookieName is the cookie extractToken reads when no header token is present.
|
|
const DocsCookieName = "session"
|
|
|
|
// DocsCookiePath scopes the cookie to the docs pages only.
|
|
const DocsCookiePath = "/api/docs"
|
|
|
|
// SetDocsSessionCookie stores the session token for the docs pages.
|
|
func SetDocsSessionCookie(w http.ResponseWriter, r *http.Request, token string, expires time.Time) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: DocsCookieName,
|
|
Value: token,
|
|
Path: DocsCookiePath,
|
|
Expires: expires,
|
|
HttpOnly: true,
|
|
Secure: requestIsSecure(r),
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
// ClearDocsSessionCookie removes the docs cookie (logout).
|
|
func ClearDocsSessionCookie(w http.ResponseWriter, r *http.Request) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: DocsCookieName,
|
|
Value: "",
|
|
Path: DocsCookiePath,
|
|
MaxAge: -1,
|
|
HttpOnly: true,
|
|
Secure: requestIsSecure(r),
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
func requestIsSecure(r *http.Request) bool {
|
|
return r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
|
|
}
|