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
This commit is contained in:
36
server/api/__journal__/cache/purge.post.ts
vendored
Normal file
36
server/api/__journal__/cache/purge.post.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 }
|
||||
})
|
||||
Reference in New Issue
Block a user