Files
Jakub Zych a03e076c62 feat: token-protected site cache purge route
- POST /api/__journal__/cache/purge removes every Nitro route-cache entry
- 404 without a 32-character token or behind a proxy, 401 on a wrong bearer
  token (constant-time over sha256 digests)
- routeRules keep /api/__journal__/** out of the route cache
- runtimeConfig.journal.cachePurgeToken (NUXT_JOURNAL_CACHE_PURGE_TOKEN)
- fixture API reload(); testing/purge-check.mjs proves a cached page shows
  a change after a purge; the playground caches with swr 300
2026-10-10 18:19:58 +02:00

37 lines
1.7 KiB
TypeScript

// POST /api/__journal__/cache/purge: removes every Nitro route-cache entry
// (pages, _payload.json and feeds), so a Journal save in the admin shows on
// the next page view. The backend calls it on loopback after the save
// commits, with `Authorization: Bearer <runtimeConfig.journal.cachePurgeToken>`
// and the body {"kind":"post"|"category","slug":"<slug>"}; the answer is
// {"purged":<n>}.
//
// 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);
// 401 for a missing or wrong bearer token. The layer's routeRules keep this
// route out of the route cache.
import { authorizePurge, parsePurgeEvent, purgeRouteCache } from '../../../../app/utils/siteCache.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 verdict = authorizePurge({
token: (config.journal as { cachePurgeToken?: unknown }).cachePurgeToken,
authorization: getRequestHeader(event, 'authorization'),
proxied: PROXY_HEADERS.some((name) => getRequestHeader(event, name) !== undefined),
})
if (verdict === 'disabled' || verdict === 'proxied') {
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
}
if (verdict === 'unauthorized') {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
const { kind, slug } = parsePurgeEvent(await readBody(event).catch(() => null))
const purged = await purgeRouteCache(useStorage('cache'))
console.info(`[journal] cache purge: removed ${purged} route cache entries (${kind} ${slug})`)
return { purged }
})