Files
xamxam/scripts/deploy-server.sh
T
Pontoporeia 554ba3ee8d fix(admin): stop logging out active long-form work; raise idle timeout to 4h
The admin idle timeout (30 min) was refreshed only by navigations and HTMX
requests. During long encoding sessions on an open form there are none, so
an actively-typing admin was logged out mid-work after ~30-45 min.

Add an activity-driven keepalive:
- /admin/session-keepalive.php: 204 when authenticated (refreshes
  admin_last_activity via AdminAuth::isAuthenticated()), 401 otherwise.
- admin-session-keepalive.js: marks activity only on real user input
  (pointer/keyboard/input/scroll/wheel/touch/focus) and pings at most once
  per 5 min while the tab is visible. A genuinely idle tab never pings, so
  the idle timeout still applies.

Raise the idle window 30 min -> 4 h: for a single-/few-admin back-office
whose main workflow is data entry, 30 min still kicked admins who stepped
away mid-form. With the keepalive in place, 4 h means "no interaction at
all", not "no navigation". Absolute timeout stays 12 h.

Also fix session ID rotation, which never fired: it used
`$absolute % IDLE_TIMEOUT_SECONDS === 0`, i.e. required a request to land
exactly on a multiple of the interval relative to login time. Replaced with
an explicit admin_last_rotation timestamp and a ROTATION_INTERVAL_SECONDS
(30 min) constant decoupled from the idle timeout, so raising the idle
window does not widen the fixation/replay window.

Refactor AdminAuth::enforceSessionTimeout() to return bool instead of
redirecting/exiting, so the keepalive endpoint can report 401 cleanly
rather than letting fetch follow a redirect to the login page.

Smoke test (just smoke-session-keepalive) covers activity refresh, 2 h idle
accepted, rotation firing, idle rejection+destruction, and unauthenticated
rejection. Docs updated.
2026-09-18 16:26:49 +02:00

203 lines
8.3 KiB
Bash
Executable File

#!/bin/bash
# Deploy production nginx configuration for XAMXAM
# Fixes permissions and installs /tmp/xamxam.conf into nginx sites-available.
#
# Usage: just deploy-nginx (uploads script + config, then runs this)
# or: sudo bash /tmp/deploy-server.sh
set -e
# ── Colors ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
err() { printf "${RED}✗${NC} %s\n" "$*" >&2; }
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
# ─────────────────────────────────────────────────────────────────────────────
[ "$EUID" -eq 0 ] || { err "Run as root (sudo)"; exit 1; }
printf "🚀 XAMXAM Production Deployment\n"
printf "================================\n\n"
# ── Step 0: System dependencies ───────────────────────────────────────────────
printf "📋 Step 0: Checking system dependencies...\n"
echo "--------------------------------------"
if ! php -m | grep -qi curl; then
apt-get install -y php-curl
ok "Installed php-curl"
else
ok "php-curl already installed"
fi
if ! command -v sqlite3 &>/dev/null; then
apt-get install -y sqlite3
ok "Installed sqlite3"
else
ok "sqlite3 already installed"
fi
# ── Step 1: Permissions ───────────────────────────────────────────────────────
printf "📋 Step 1: Fixing file permissions...\n"
echo "--------------------------------------"
chown -R www-data:xamxam /var/www/xamxam/
ok "Ownership: www-data:xamxam"
find /var/www/xamxam -type d -exec chmod 2775 {} \;
ok "Directories: 2775 (setgid)"
find /var/www/xamxam -type f -exec chmod 664 {} \;
ok "Files: 664"
if [ -d "/var/www/xamxam/storage" ]; then
chmod 2775 /var/www/xamxam/storage
find /var/www/xamxam/storage -name "*.db" -exec chmod 660 {} \;
# SQLite WAL sidecar files must also be writable
find /var/www/xamxam/storage -name "*.db-wal" -exec chmod 660 {} \;
find /var/www/xamxam/storage -name "*.db-shm" -exec chmod 660 {} \;
ok "Storage: 2775, databases (+WAL/SHM): 660"
fi
# .env must be 640 (contains secrets)
if [ -f "/var/www/xamxam/.env" ]; then
chmod 640 /var/www/xamxam/.env
ok ".env: 640"
fi
# Migrate posterg.db → xamxam.db if the new name is missing or empty
if [ -f "/var/www/xamxam/storage/posterg.db" ]; then
if [ ! -s "/var/www/xamxam/storage/xamxam.db" ]; then
cp /var/www/xamxam/storage/posterg.db /var/www/xamxam/storage/xamxam.db
ok "Migrated posterg.db → xamxam.db"
fi
rm /var/www/xamxam/storage/posterg.db
ok "Removed legacy posterg.db"
fi
# Ensure writable cache subdirectories exist for php-fpm (www-data)
mkdir -p /var/www/xamxam/storage/cache/rate_limit
chown -R www-data:xamxam /var/www/xamxam/storage/cache
chmod -R 2775 /var/www/xamxam/storage/cache
ok "Cache dirs: created and owned by www-data:xamxam"
# Ensure PHP upload temp dir exists on storage partition (not /tmp tmpfs)
mkdir -p /var/www/xamxam/storage/tmp/php-uploads
chown www-data:xamxam /var/www/xamxam/storage/tmp/php-uploads
chmod 2775 /var/www/xamxam/storage/tmp/php-uploads
ok "PHP upload temp dir: /var/www/xamxam/storage/tmp/php-uploads"
# Ensure the application log directory exists and is writable by php-fpm.
# App logs (Monolog) go to /var/log/xamxam/xamxam-{channel}-YYYY-MM-DD.log in
# production; without this the logger falls back to NullHandler (silent).
mkdir -p /var/log/xamxam
chown www-data:xamxam /var/log/xamxam
chmod 2775 /var/log/xamxam
ok "Log dir: /var/log/xamxam owned by www-data:xamxam (2775)"
# PHP-FPM session GC must not reap active admin sessions early.
# The app enforces its own server-side idle/absolute timeouts in AdminAuth
# (4 h idle / 12 h absolute), so session.gc_maxlifetime needs to be at
# least the absolute timeout, and GC re-enabled to clean up stale files.
PHP_FPM_INI="/etc/php/8.4/fpm/conf.d/zz-xamxam-session.ini"
cat > "$PHP_FPM_INI" <<'INI'
; XAMXAM session tuning.
; AdminAuth enforces its own idle/absolute timeouts (4 h / 12 h), so
; gc_maxlifetime must be >= the absolute timeout or PHP would reap active
; sessions from under the app.
session.gc_maxlifetime = 43200
session.gc_probability = 1
session.gc_divisor = 100
INI
ok "PHP-FPM session GC: $PHP_FPM_INI"
# Backups dir must be writable by both www-data (cron) and the deploy user
# (xamxam group) so scripts/migrate.sh can write a pre-deploy snapshot before
# running migrations.
mkdir -p /var/backups/xamxam
chown www-data:xamxam /var/backups/xamxam
chmod 2775 /var/backups/xamxam
ok "Backup dir: /var/backups/xamxam owned by www-data:xamxam (2775)"
# App var/ dirs (cache/logs/tmp) checked by deploy-verify-permissions.
mkdir -p /var/www/xamxam/var/{cache,logs,tmp}
chown -R www-data:xamxam /var/www/xamxam/var
chmod -R 2775 /var/www/xamxam/var
ok "var/ dirs: /var/www/xamxam/var/{cache,logs,tmp}"
# ── Step 2: Nginx config ──────────────────────────────────────────────────────
printf "\n📋 Step 2: Deploying nginx configuration...\n"
echo "--------------------------------------------"
if [ ! -f "/tmp/xamxam.conf" ]; then
err "/tmp/xamxam.conf not found — run: just deploy-nginx"
exit 1
fi
if [ -f "/etc/nginx/sites-available/xamxam" ]; then
cp /etc/nginx/sites-available/xamxam \
"/etc/nginx/sites-available/xamxam.backup.$(date +%Y%m%d_%H%M%S)"
ok "Backed up existing config"
fi
cp /tmp/xamxam.conf /etc/nginx/sites-available/xamxam
ok "Installed new nginx config"
# Remove legacy posterg symlink if it exists (causes duplicate limit_req_zone)
if [ -L "/etc/nginx/sites-enabled/posterg" ]; then
rm /etc/nginx/sites-enabled/posterg
ok "Removed legacy sites-enabled/posterg symlink"
fi
# Remove legacy posterg config and all its backups from sites-available
for f in /etc/nginx/sites-available/posterg /etc/nginx/sites-available/posterg.backup.*; do
[ -f "$f" ] && rm "$f" && ok "Removed legacy $f"
done
# Keep only the 2 most recent xamxam backups, delete older ones
ls -t /etc/nginx/sites-available/xamxam.backup.* 2>/dev/null | tail -n +3 | xargs -r rm --
ok "Pruned old xamxam config backups (kept 2 most recent)"
if [ ! -L "/etc/nginx/sites-enabled/xamxam" ]; then
ln -s /etc/nginx/sites-available/xamxam /etc/nginx/sites-enabled/xamxam
ok "Created sites-enabled symlink"
fi
# ── Step 3: Validate ──────────────────────────────────────────────────────────
printf "\n📋 Step 3: Testing nginx configuration...\n"
echo "------------------------------------------"
if nginx -t 2>&1; then
ok "Nginx configuration is valid"
else
err "Nginx configuration has errors — restoring backup"
latest=$(ls -t /etc/nginx/sites-available/xamxam.backup.* 2>/dev/null | head -1)
[ -n "$latest" ] && cp "$latest" /etc/nginx/sites-available/xamxam
exit 1
fi
# ── Step 4: Reload nginx ─────────────────────────────────────────────────────
printf "\n"
echo "📋 Step 4: Reloading nginx..."
echo "------------------------------"
systemctl reload nginx
ok "Nginx reloaded"
# Reload PHP-FPM so the session GC settings take effect.
systemctl reload php8.4-fpm 2>/dev/null || systemctl reload php-fpm 2>/dev/null || true
ok "PHP-FPM reloaded"
# ── Done ──────────────────────────────────────────────────────────────────────
printf "\n"
ok "Permissions fixed"
ok "Nginx config installed"
ok "Configuration validated"
ok "Nginx reloaded"
printf "\nVerify:\n"
printf " https://xamxam.erg.be/\n"
printf " https://xamxam.erg.be/admin/\n"
printf " https://xamxam.erg.be/storage/xamxam.db (should 403/404)\n"