Files
EpicNext-Cms/scripts/cf-setup-cache.sh
T
openhands 7697728d07
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 28s
CI / tests-unit (push) Successful in 1m39s
CI / tests-integration (push) Successful in 1m41s
CI / tests-ui (push) Successful in 2m27s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m35s
feat(cache): single-owner caching across nginx, edge and content edits
Rebuild production nginx from the repo (deployment/proxy/*) with a single
Cache-Control owner per route: the app stays the source, nginx only manages
headers, and Cloudflare stores the public API allowlist at the edge.

- deployment/proxy: nginx.conf, mime.types, nginx-cms.conf and the
  blue/green upstream snippet; config backed by scripts/nginx-sync.sh
  (idempotent install + reload, --check/--force).
- nginx serves Cache-Tag headers on the public allowlist (cms-public),
  gamedata, client and camera responses so the edge and purge stay in sync.
- src/lib/edge-cache.ts + tests: coalesced, fire-and-forget edge purges that
  no-op unless Cloudflare is configured; scripts/cf-purge.sh and
  cf-setup-cache.sh create and purge the cache rule.
- src/lib/cloudflare-api.ts: purgeCacheByTags/purgeCacheByUrls.
- Purge hooks after catalog exports (public + gamedata) and on shop, team,
  guild, photo and rare-values edits; ci-deploy purges after each release.
- src/proxy.ts excludes the imaging/images docs from the middleware matcher.
2026-09-28 21:55:18 +02:00

125 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Create/update the Cloudflare Cache Rule that stores the CMS public API
# allowlist at the edge (the routes nginx tags with `Cache-Tag: cms-public`).
#
# Why a rule is required: Cloudflare only caches a handful of file extensions
# by default; `/api/*` responses are served `cf-cache-status: DYNAMIC` even
# though their `Cache-Control: s-maxage` says they are cacheable. A Cache Rule
# with "Cache Everything" turns those the other way.
#
# What the rule does:
# - edge_ttl bypass_by_default : edge cachet volgens de s-maxage van nginx;
# zonder (publieke) header (bv. errorresponses) juist NIET cachen.
# - browser_ttl respect_origin : de zone heeft "Browser Cache TTL = 1 jaar" en
# overschrijft daarmee het max-age dat nginx per klasse stuurt. Deze rule
# herstelt dat voor de publieke API's: browsers krijgen de korte
# max-age van nginx terug (10/60/300s) i.p.v. een jaar stale data.
#
# Idempotent: vergelijkt de bestaande rule (op description + inhoud) en zet
# alleen bij als die verschilt. Re-running is veilig.
#
# Usage (after putting a real token + zone id in .env):
# scripts/cf-setup-cache.sh
#
# Requires a token with Zone > Cache Rules (edit) permission.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"
BASE="https://api.cloudflare.com/client/v4"
PHASE="http_request_cache_settings"
DESCRIPTION="EpicNabbo CMS public API edge cache (cms-public)"
# Cache de allowlist exact zoals nginx hem tagt (deployment/proxy/nginx-cms.conf).
# Geen regex: `matches` vereist Business; vrije operators zijn `in` en
# `starts_with()`.
EXPRESSION='(http.request.method eq "GET") and (http.request.uri.path in { "/api/staff" "/api/teams" "/api/guilds" "/api/photos" "/api/leaderboard" "/api/online" "/api/online/count" "/api/shop" "/api/shop/categories" "/api/values" "/api/values/categories" "/api/radio/current-dj" "/api/radio/points/leaderboard" } or starts_with(http.request.uri.path, "/api/values/"))'
load_env() {
local name="$1"
if [[ -n "${!name:-}" ]]; then
printf -v "$name" '%s' "${!name}"
return 0
fi
if [[ -f "$ENV_FILE" ]]; then
local line
line="$(grep -m1 "^$name=" "$ENV_FILE" | cut -d= -f2- | tr -d "'\"")" || true
if [[ -n "$line" ]]; then
printf -v "$name" '%s' "$line"
return 0
fi
fi
return 1
}
load_env CLOUDFLARE_API_TOKEN || { echo "error: CLOUDFLARE_API_TOKEN not configured" >&2; exit 1; }
load_env CLOUDFLARE_ZONE_ID || { echo "error: CLOUDFLARE_ZONE_ID not configured" >&2; exit 1; }
if [[ "${#CLOUDFLARE_API_TOKEN}" -lt 16 || "${#CLOUDFLARE_ZONE_ID}" -lt 16 ]]; then
echo "error: Cloudflare credentials look like placeholders; add a real token to .env" >&2
exit 1
fi
api() {
curl -sS -m 30 -X "$1" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data "${2:-}" \
"$BASE/zones/$CLOUDFLARE_ZONE_ID${3:-}"
}
echo "--- reading existing cache-settings ruleset ---"
existing="$(api GET "" "/rulesets/phases/$PHASE/entrypoint")"
if ! python3 -c 'import json,sys; sys.exit(0 if json.load(sys.stdin).get("success") else 1)' <<<"$existing"; then
echo "error: could not read ruleset: $existing" >&2
exit 1
fi
rule_json="$(DESCRIPTION="$DESCRIPTION" EXPRESSION="$EXPRESSION" python3 - <<'PY'
import json, os
print(json.dumps({
"description": os.environ["DESCRIPTION"],
"expression": os.environ["EXPRESSION"],
"action": "set_cache_settings",
"action_parameters": {
"cache": True,
"edge_ttl": {"mode": "bypass_by_default"},
"browser_ttl": {"mode": "respect_origin"},
},
}))
PY
)"
out="$(EXISTING_JSON="$existing" RULE_JSON="$rule_json" python3 - <<'PY'
import json, os
existing = json.loads(os.environ["EXISTING_JSON"])
rule = json.loads(os.environ["RULE_JSON"])
result = existing.get("result") or {}
rules = list(result.get("rules") or [])
def check(r):
return {k: r.get(k) for k in ("description", "expression", "action", "action_parameters")}
keep = [r for r in rules if r.get("description") != rule["description"]]
present = [r for r in rules if r.get("description") == rule["description"]]
if present and check(present[0]) == check(rule):
print("same")
else:
keep.append(rule)
print("changed")
print(json.dumps({"rules": keep}))
PY
)"
status="$(sed -n '1p' <<<"$out")"
if [ "$status" = "same" ]; then
echo "rule already present en identiek — geen wijzigingen"
exit 0
fi
payload="$(sed -n '2,$p' <<<"$out")"
echo "--- ${DESCRIPTION}: rule bijwerken ---"
resp="$(api PUT "$payload" "/rulesets/phases/$PHASE/entrypoint")"
if ! python3 -c 'import json,sys; sys.exit(0 if json.load(sys.stdin).get("success") else 1)' <<<"$resp"; then
echo "error: could not save ruleset: $resp" >&2
exit 1
fi
echo "cache rule live. Verify: curl -s https://epicnabbo.nl/api/shop -o /dev/null -D - | grep -i cf-cache-status"