default: @just --list # XAMXAM Justfile # ============================================================================ # Development # ============================================================================ [group('dev')] setup: @bash scripts/setup-dev.sh [group('dev')] serve: migrate @xdg-open http://127.0.0.1:8000 & @xdg-open http://127.0.0.1:8000/admin/ & @php \ -d upload_max_filesize=512M \ -d post_max_size=520M \ -d memory_limit=256M \ -d max_execution_time=300 \ -d max_input_time=300 \ -S 127.0.0.1:8000 -t app/public/ app/router.php 2>&1 \ | stdbuf -oL grep -Ev '(Accepted|Closing|live-reload\.php|assets/|favicon)' [group('dev')] stop: @pkill -f "php -S 127.0.0.1:8000" 2>/dev/null && echo "stopped" || echo "no server running" [group('dev')] logs: @tail -n 20 error.log 2>/dev/null || echo "no error log" # ============================================================================ # Deploy # ============================================================================ [group('deploy')] deploy: # Main deploy (code + assets) then run any pending DB migrations rsync -vur --progress --delete \ --chown="www-data:xamxam" \ --exclude 'vendor' \ --exclude 'tests' \ --exclude '*.md' \ --exclude '.git*' \ --exclude '.jj' \ --exclude '.claude' \ --exclude '.pi' \ --exclude '.DS_Store' \ --exclude '.env' \ --exclude 'storage/xamxam.db' \ --exclude 'storage/theses' \ --exclude 'storage/covers' \ --exclude 'storage/backup_*' \ --exclude 'storage/cache/*' \ --exclude 'storage/maintenance.flag' \ --exclude 'storage/fixtures' \ --exclude 'storage/docs' \ --exclude 'var/' \ app/ xamxam:/var/www/xamxam/ # Upload deploy-server.sh for post-deploy permission fix rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh ssh xamxam "sudo bash /tmp/deploy-server.sh" ssh xamxam "rm -f /tmp/deploy-server.sh" ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}" ssh xamxam "cd /var/www/xamxam && php -r 'if (!file_exists(\"/var/www/xamxam/storage/xamxam.db\")) { \$db = new PDO(\"sqlite:/var/www/xamxam/storage/xamxam.db\"); \$db->exec(file_get_contents(\"/var/www/xamxam/storage/schema.sql\")); echo \"Database created from schema.\\n\"; } else { echo \"Database already exists.\\n\"; }'" # Run pending migrations ssh xamxam "cd /var/www/xamxam && bash scripts/migrate.sh" # Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys) @just deploy-env @just deploy-verify-permissions [group('deploy')] deploy-env: #!/usr/bin/env bash set -euo pipefail # Upload app/.env only if it exists locally; never overwrites a remote .env that already has APP_KEY. if [ ! -f app/.env ]; then echo "WARNING: app/.env not found locally β€” skipping." exit 0 fi if ssh xamxam '[ -f /var/www/xamxam/.env ]'; then echo "Remote .env already exists β€” skipping to avoid overwriting key." echo "Run 'just reencrypt-password' if you rotated APP_KEY." else rsync -v --progress app/.env xamxam:/var/www/xamxam/.env ssh xamxam "chmod 640 /var/www/xamxam/.env && chown www-data:xamxam /var/www/xamxam/.env" echo ".env uploaded." fi [group('deploy')] reencrypt-password new_key_b64="": #!/usr/bin/env bash set -euo pipefail # Re-encrypt the SMTP password in the remote DB after rotating APP_KEY. # Usage: # 1. Generate a new key: php -r "echo base64_encode(random_bytes(32));" # 2. Run: just reencrypt-password # 3. Update app/.env locally with the new key, then run: just deploy-env if [ -z "{{new_key_b64}}" ]; then echo "Usage: just reencrypt-password " echo "Generate a key: php -r \"echo base64_encode(random_bytes(32));\"" exit 1 fi # Run the re-encryption script on the server using the current key (from remote .env) # and the supplied new key. ssh xamxam "php /var/www/xamxam/scripts/reencrypt-smtp-password.php '{{new_key_b64}}' /var/www/xamxam/storage/xamxam.db" [group('deploy')] deploy-db: @ssh xamxam '[ ! -f /var/www/xamxam/storage/xamxam.db ]' || (echo "ERROR: remote database already exists. Remove it manually if you intend to overwrite." && exit 1) rsync -v --progress app/storage/xamxam.db xamxam:/var/www/xamxam/storage/xamxam.db ssh xamxam "chown www-data:xamxam /var/www/xamxam/storage/xamxam.db && chmod 660 /var/www/xamxam/storage/xamxam.db" [group('deploy')] deploy-verify-permissions: #!/usr/bin/env bash set -euo pipefail APP_DIR="/var/www/xamxam" WEB_USER="www-data" APP_GROUP="xamxam" ERRORS=0 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; ERRORS=$((ERRORS + 1)); } warn() { printf "${YELLOW}!${NC} %s\n" "$*"; } printf "πŸ” Verifying permissions on %s…\n\n" "$APP_DIR" # ── Ownership ────────────────────────────────────────────────────────────────── echo "── Ownership ───────────────────────────────────" while IFS= read -r line; do owner=$(echo "$line" | awk '{print $3}') group=$(echo "$line" | awk '{print $4}') path=$(echo "$line" | awk '{print $NF}') if [ "$owner" != "$WEB_USER" ] || [ "$group" != "$APP_GROUP" ]; then err "$path β†’ $owner:$group (expected $WEB_USER:$APP_GROUP)" else ok "$path β†’ $owner:$group" fi done < <(ssh xamxam "stat -c '%U %G %n' $APP_DIR $APP_DIR/app $APP_DIR/storage $APP_DIR/var 2>/dev/null") # ── Key directories: 2775 ───────────────────────────────────────────────────── echo "── Directory permissions (expected 2775) ───────" while IFS= read -r line; do perms=$(echo "$line" | awk '{print $1}') path=$(echo "$line" | awk '{print $NF}') if [ "$perms" != "drwxrwsr-x" ]; then err "$path β†’ $perms (expected drwxrwsr-x / 2775)" else ok "$path β†’ $perms" fi done < <(ssh xamxam "find $APP_DIR -maxdepth 2 -type d -exec stat -c '%A %n' {} \\; 2>/dev/null | sort") # ── Key files: 664 ──────────────────────────────────────────────────────────── echo "── File permissions (expected 664 / 660) ───────" # Spot-check a few critical files while IFS= read -r path; do perms=$(ssh xamxam "stat -c '%a %U %G' '$path' 2>/dev/null" || echo "MISSING") if [ "$perms" = "MISSING" ]; then err "$path β†’ FILE MISSING" else perm_num=$(echo "$perms" | awk '{print $1}') owner=$(echo "$perms" | awk '{print $2}') group=$(echo "$perms" | awk '{print $3}') case "$path" in */storage/xamxam.db|*/storage/*.db) expected_perm="660" ;; *) expected_perm="664" ;; esac if [ "$perm_num" != "$expected_perm" ]; then err "$path β†’ $perm_num ($owner:$group), expected $expected_perm $WEB_USER:$APP_GROUP" elif [ "$owner" != "$WEB_USER" ]; then err "$path β†’ owner $owner, expected $WEB_USER (perm $perm_num OK)" else ok "$path β†’ $perm_num $owner:$group" fi fi done < <(printf '%s\n' \ "$APP_DIR/.env" \ "$APP_DIR/app/router.php" \ "$APP_DIR/storage/xamxam.db") # ── var/ subdirectories must be writable ────────────────────────────────────── echo "── var/ writability ────────────────────────────" for subdir in cache logs tmp; do if ssh xamxam "[ -w /var/www/xamxam/var/$subdir ]"; then ok "var/$subdir β†’ writable" else err "var/$subdir β†’ NOT WRITABLE" fi done # ── storage/cache/rate_limit writable ───────────────────────────────────────── if ssh xamxam "[ -w /var/www/xamxam/storage/cache/rate_limit ]"; then ok "storage/cache/rate_limit β†’ writable" else err "storage/cache/rate_limit β†’ NOT WRITABLE" fi # ── .env must be 640 ────────────────────────────────────────────────────────── env_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "") if [ "$env_perm" = "640" ]; then ok ".env β†’ 640" elif [ -z "$env_perm" ]; then warn ".env β†’ MISSING" else err ".env β†’ $env_perm (expected 640)" fi # ── Summary ─────────────────────────────────────────────────────────────────── echo "" if [ "$ERRORS" -eq 0 ]; then printf "${GREEN}βœ… All permissions OK${NC}\n" else printf "${RED}❌ %d permission error(s) found${NC}\n" "$ERRORS" printf "${YELLOW}Fix with: sudo bash /tmp/deploy-server.sh${NC}\n" exit 1 fi [group('deploy')] deploy-script script_name: # Generic script deployer (e.g., just deploy-script setup-server) rsync -v scripts/{{script_name}}.sh xamxam:/tmp/{{script_name}}.sh @echo "" @echo "Script uploaded. SSH into the server and run:" @echo "" @echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh" @echo "" # ============================================================================ # Testing # ============================================================================ [group('test')] test: # Run all tests. To run a subset, use: # php app/tests/Unit/DatabaseTest.php # php app/tests/Integration/SearchTest.php @php app/tests/run-tests.php [group('test')] lint-biome: @biome lint app/public/assets/js/ [group('test')] phpstan: @vendor/bin/phpstan analyse --memory-limit=512M [group('test')] cs-check: @vendor/bin/php-cs-fixer check --no-interaction [group('test')] cs-fix: @vendor/bin/php-cs-fixer fix --no-interaction [group('test')] syntax: @find app/ -name '*.php' -exec php -l {} \; 2>/dev/null | grep -v 'No syntax errors' || true @echo 'βœ… Syntax OK' # ============================================================================ # Database # ============================================================================ [group('database')] migrate: @echo "Running migrations…" @bash scripts/migrate.sh [group('database')] init-db: @sqlite3 app/storage/xamxam.db < app/storage/schema.sql @sqlite3 app/storage/xamxam.db "SELECT COUNT(*) || ' tables' FROM sqlite_master WHERE type='table';" [group('database')] reset-db: @rm -f app/storage/xamxam.db @just init-db [group('database')] query: @sqlite3 app/storage/xamxam.db [group('database')] backup: @sqlite3 app/storage/xamxam.db .dump > app/storage/backup_$(date +%Y%m%d_%H%M%S).sql [group('database')] backup-snapshot: # Hot backup using SQLite's .backup API (WAL-safe), then gzip. @DB_PATH=app/storage/xamxam.db BACKUP_DIR=app/storage/backups RETENTION_DAYS=30 bash scripts/backup-sqlite.sh # ============================================================================ # Utils # ============================================================================ [group('utils')] clean: @rm -f app/error.log @rm -rf app/storage/cache/rate_limit/* @rm -f /tmp/xamxam-*.log /tmp/xamxam-*.pid