fix: harden deploys and close top security holes after /events outage
Isolate next dev from production .next, add build-guard/atomic deploy/health watchdog, error boundaries, and fix JWT startup, meetup leaks, media path traversal, SVG/memory uploads, and JSON-LD escaping. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Atomic frontend deploy for the Belgian Bitcoin Embassy site.
|
||||
#
|
||||
# Builds into a scratch dist dir, verifies the artifact, then swaps it into place
|
||||
# as .next in a single mv (rename is atomic on the same filesystem). The running
|
||||
# server therefore never sees a half-written .next, and a failed build leaves the
|
||||
# current production build untouched.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/deploy-frontend.sh # build, verify, swap, restart service
|
||||
# scripts/deploy-frontend.sh --no-restart # build + swap, skip systemctl
|
||||
#
|
||||
# Requires sudo only for the final `systemctl restart` (skip with --no-restart).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
FRONTEND_DIR="${REPO_DIR}/frontend"
|
||||
BUILD_DIR="${FRONTEND_DIR}/.next-build"
|
||||
PROD_DIR="${FRONTEND_DIR}/.next"
|
||||
BACKUP_DIR="${FRONTEND_DIR}/.next-prev"
|
||||
MIN_FREE_MB="${MIN_FREE_MB:-600}"
|
||||
|
||||
RESTART=1
|
||||
[ "${1:-}" = "--no-restart" ] && RESTART=0
|
||||
|
||||
log() { echo "deploy-frontend: $*"; }
|
||||
die() { echo "deploy-frontend: ERROR: $*" >&2; exit 1; }
|
||||
|
||||
cd "${FRONTEND_DIR}"
|
||||
|
||||
# A production `next build` on this box needs headroom; bail early with a clear
|
||||
# message rather than getting OOM-killed halfway through.
|
||||
free_mb="$(free -m | awk '/^Mem:/ {print $7}')"
|
||||
if [ -n "${free_mb}" ] && [ "${free_mb}" -lt "${MIN_FREE_MB}" ]; then
|
||||
die "only ${free_mb}MB available memory (< ${MIN_FREE_MB}MB). Free memory or set MIN_FREE_MB=... to override."
|
||||
fi
|
||||
|
||||
log "building into ${BUILD_DIR} ..."
|
||||
rm -rf "${BUILD_DIR}"
|
||||
NEXT_DIST_DIR=".next-build" npm run build
|
||||
|
||||
log "verifying fresh build ..."
|
||||
NEXT_DIR="${BUILD_DIR}"
|
||||
[ -f "${NEXT_DIR}/BUILD_ID" ] || die "build produced no BUILD_ID"
|
||||
[ -f "${NEXT_DIR}/build-manifest.json" ] || die "build produced no build-manifest.json"
|
||||
[ -f "${NEXT_DIR}/prerender-manifest.json" ] || die "build produced no prerender-manifest.json"
|
||||
[ -d "${NEXT_DIR}/static/development" ] && die "fresh build contains static/development (dev contamination)"
|
||||
grep -q "static/development" "${NEXT_DIR}/build-manifest.json" && die "build-manifest references static/development"
|
||||
|
||||
log "swapping ${BUILD_DIR} -> ${PROD_DIR} (atomic) ..."
|
||||
rm -rf "${BACKUP_DIR}"
|
||||
[ -d "${PROD_DIR}" ] && mv "${PROD_DIR}" "${BACKUP_DIR}"
|
||||
mv "${BUILD_DIR}" "${PROD_DIR}"
|
||||
|
||||
log "deployed BUILD_ID=$(cat "${PROD_DIR}/BUILD_ID")"
|
||||
|
||||
if [ "${RESTART}" -eq 1 ]; then
|
||||
log "restarting bbe-frontend.service ..."
|
||||
sudo systemctl restart bbe-frontend
|
||||
log "done. previous build kept at ${BACKUP_DIR}"
|
||||
else
|
||||
log "skipped restart (--no-restart). previous build kept at ${BACKUP_DIR}"
|
||||
fi
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Health watchdog for the BBE site. Probes the frontend's request-time routes and
|
||||
# the backend health endpoint on their local ports. On sustained failure it
|
||||
# restarts bbe-frontend and logs loudly to the journal, turning a silent multi-day
|
||||
# outage (like the /events 500) into a self-healing few-minute blip.
|
||||
#
|
||||
# Meant to be run by bbe-site-healthcheck.timer every few minutes. State is kept
|
||||
# in a tmp file so a single transient blip doesn't trigger a restart — we only act
|
||||
# after FAIL_THRESHOLD consecutive failing runs.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
FRONTEND_BASE="${FRONTEND_BASE:-http://127.0.0.1:4056}"
|
||||
API_BASE="${API_BASE:-http://127.0.0.1:4055}"
|
||||
FAIL_THRESHOLD="${FAIL_THRESHOLD:-2}"
|
||||
STATE_FILE="${STATE_FILE:-/tmp/bbe-healthcheck.fails}"
|
||||
TIMEOUT="${TIMEOUT:-10}"
|
||||
|
||||
FRONTEND_PATHS=(/ /events /blog)
|
||||
|
||||
log() { echo "bbe-healthcheck: $*"; }
|
||||
|
||||
check() {
|
||||
local url="$1" code
|
||||
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time "${TIMEOUT}" "${url}" 2>/dev/null || echo 000)"
|
||||
if [ "${code}" = "200" ]; then
|
||||
return 0
|
||||
fi
|
||||
log "FAIL ${url} -> ${code}"
|
||||
return 1
|
||||
}
|
||||
|
||||
failed=0
|
||||
for p in "${FRONTEND_PATHS[@]}"; do
|
||||
check "${FRONTEND_BASE}${p}" || failed=1
|
||||
done
|
||||
check "${API_BASE}/api/health" || failed=1
|
||||
|
||||
if [ "${failed}" -eq 0 ]; then
|
||||
# Healthy: clear the failure counter and exit quietly.
|
||||
rm -f "${STATE_FILE}"
|
||||
log "OK (all probes 200)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Unhealthy: increment consecutive-failure counter.
|
||||
count=0
|
||||
[ -f "${STATE_FILE}" ] && count="$(cat "${STATE_FILE}" 2>/dev/null || echo 0)"
|
||||
count=$((count + 1))
|
||||
echo "${count}" > "${STATE_FILE}"
|
||||
log "unhealthy run ${count}/${FAIL_THRESHOLD}"
|
||||
|
||||
if [ "${count}" -ge "${FAIL_THRESHOLD}" ]; then
|
||||
log "threshold reached — restarting bbe-frontend.service"
|
||||
if systemctl restart bbe-frontend 2>/dev/null; then
|
||||
log "restart issued"
|
||||
else
|
||||
log "ERROR: restart failed (needs privileges? see PolicyKit/sudoers note in ops/README.md)"
|
||||
fi
|
||||
rm -f "${STATE_FILE}"
|
||||
fi
|
||||
|
||||
exit 1
|
||||
Reference in New Issue
Block a user