Files
vue-journal-layer/server/api/__site__/cache/purge.post.ts
Jakub Zych f5d7aacf11 feat(19.3-06): purge route accepts proxied requests on siteCache.allowProxied (WR-03)
- runtimeConfig.siteCache.allowProxied (NUXT_SITE_CACHE_ALLOW_PROXIED, default
  false) lets the shared purge route accept a request that came through a
  TLS-terminating proxy; the bearer token is then the only gate
- shared lib, route and spec stay byte-identical across the three layers,
  with updated sha256 pins and two new sitePurge specs

- version 0.5.0
2026-10-15 22:42:09 +02:00

45 lines
2.2 KiB
TypeScript

// POST /api/__site__/cache/purge: removes every Nitro route-cache entry
// (pages, _payload.json and feeds), so an admin save shows on the next page
// view. The backend calls it on loopback after the save commits, with
// `Authorization: Bearer <runtimeConfig.siteCache.purgeToken>` and the body
// {"kind":"<kind>","slug":"<slug>"}; the answer is {"purged":<n>}. The kind
// and slug only reach the log line.
//
// 404 without a token of at least 32 characters, and for any request that
// came through a reverse proxy (X-Forwarded-For, X-Real-IP or Forwarded)
// unless runtimeConfig.siteCache.allowProxied (NUXT_SITE_CACHE_ALLOW_PROXIED)
// is true or "true": a backend that reaches the site through a TLS-terminating
// proxy opts in, and the bearer token is then the only gate. 401 for a
// missing or wrong bearer token. The '/api/__site__/**' route rule keeps this
// route out of the route cache.
//
// Shared: several layers ship this file byte for byte, and Nitro keeps one
// handler per route and method.
import { authorizeSitePurge, parseSitePurgeEvent, purgeSiteRouteCache } from '../../../lib/siteCachePurge.ts'
const PROXY_HEADERS = ['x-forwarded-for', 'x-real-ip', 'forwarded']
export default defineEventHandler(async (event) => {
setResponseHeader(event, 'cache-control', 'no-store')
const config = useRuntimeConfig(event)
const siteCache = config.siteCache as { purgeToken?: unknown; allowProxied?: unknown } | undefined
const verdict = authorizeSitePurge({
token: siteCache?.purgeToken,
authorization: getRequestHeader(event, 'authorization'),
proxied: PROXY_HEADERS.some((name) => getRequestHeader(event, name) !== undefined),
allowProxied: siteCache?.allowProxied === true || siteCache?.allowProxied === 'true',
})
if (verdict === 'disabled' || verdict === 'proxied') {
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
}
if (verdict === 'unauthorized') {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
const { kind, slug } = parseSitePurgeEvent(await readBody(event).catch(() => null))
const purged = await purgeSiteRouteCache(useStorage('cache'))
console.info(`[site-cache] purge: removed ${purged} route cache entries (${kind} ${slug})`)
return { purged }
})