- backend.uri prefix (default /backend) mounts the admin API at {prefix}/api/v1
and the embedded SPA shell at {prefix} with an api/ JSON 404 fallback
- cookie transport: an X-Requested-With login sets the HttpOnly summer_admin
cookie and returns no token; the backend guard reads the cookie after Bearer
- CSRF wrapper refuses cookie-only POST/PUT/DELETE without X-Requested-With
- boardwalk package embeds boardwalk/dist, rewrites index.html once per prefix
and sets cache and security headers
- framework admin OpenAPI pipeline (swag, swagger2openapi, openapi-typescript)
with prefix-relative paths and typed envelopes for the tracer routes
- admin/ Vite SPA: login, plugin rail, section panel and read-only list
through the openapi-fetch client typed by the generated schema
46 lines
1.2 KiB
Go
46 lines
1.2 KiB
Go
package cabana
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// requestedWithHeader is the custom header the admin SPA sends on every
|
|
// request. A cross-site form or navigation cannot set it, and a
|
|
// cross-origin fetch that sets it needs a CORS preflight the admin API
|
|
// never answers (D-19).
|
|
requestedWithHeader = "X-Requested-With"
|
|
requestedWithAjax = "XMLHttpRequest"
|
|
)
|
|
|
|
// requireAjax refuses a state-changing admin request that is not
|
|
// Bearer-authenticated and does not carry X-Requested-With: XMLHttpRequest.
|
|
// It runs before the wrapped handler, so a refused request is never decoded,
|
|
// never looks up a controller and never reaches the database. The response
|
|
// uses the fixed D-10 code forbidden.
|
|
func requireAjax(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !csrfSafe(r) {
|
|
WriteError(w, http.StatusForbidden, "forbidden", msgForbidden)
|
|
return
|
|
}
|
|
next(w, r)
|
|
}
|
|
}
|
|
|
|
func csrfSafe(r *http.Request) bool {
|
|
switch r.Method {
|
|
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
|
return true
|
|
}
|
|
if bearerToken(r) != "" {
|
|
return true
|
|
}
|
|
return isAjax(r)
|
|
}
|
|
|
|
func isAjax(r *http.Request) bool {
|
|
return strings.TrimSpace(r.Header.Get(requestedWithHeader)) == requestedWithAjax
|
|
}
|