From 541470b9bb37a93517efe828f796c7e9f7829aa3 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Fri, 28 Aug 2026 16:56:28 +0200 Subject: [PATCH] feat(provision): idempotent setup for local dev and remote server --- app/.env | 1 - docs/deployment.md | 52 ++++++++++++++-------- docs/development.md | 34 +++++++++++---- justfile | 27 ++++++++++++ scripts/provision-server-env.sh | 58 +++++++++++++++++++++++++ scripts/provision.sh | 76 +++++++++++++++++++++++++++++++++ 6 files changed, 221 insertions(+), 27 deletions(-) delete mode 100644 app/.env create mode 100755 scripts/provision-server-env.sh create mode 100755 scripts/provision.sh diff --git a/app/.env b/app/.env deleted file mode 100644 index eaca243..0000000 --- a/app/.env +++ /dev/null @@ -1 +0,0 @@ -APP_KEY=M+4xc/c9/b4H+ScjO4cU82FzcZRO+xp5pzYNvJapUOQ= diff --git a/docs/deployment.md b/docs/deployment.md index 08f9f79..b6340c8 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -19,38 +19,56 @@ Deployment is orchestrated through the `justfile` (`deploy` group). ## One-time server setup +From a machine with `ssh` access to the `xamxam` host, run the full remote +provisioning once: + ```bash -ssh xamxam -sudo mkdir -p /var/www/xamxam -sudo chown www-data:xamxam /var/www/xamxam -sudo chmod 775 /var/www/xamxam -exit +just provision-server ``` -Then from local, deploy once and apply the nginx config + verify permissions: +This chains (each step is also runnable individually): + +1. `scripts/provision-server-env.sh` — ensure the server has an `APP_KEY` in + `/var/www/xamxam/.env` (idempotent; see below). +2. `just deploy` — code + Composer deps + migrations + permissions. +3. `just deploy-nginx` — install + apply the nginx config and fix permissions. +4. `just deploy-backup`, `just deploy-cleanup-cron`, `just deploy-logrotate` — + install backup + cleanup cron jobs and log rotation. + +It finishes by telling you what's left to do in `/admin/account` (set the admin +password — a fresh DB starts unauthenticated — and configure SMTP/PeerTube +credentials and Nextcloud sync). + +If you'd rather do it step by step (e.g. you already provisioned nginx): ```bash just deploy just deploy-nginx ``` -For a full initial rollout including backup + cleanup cron jobs: +For a full initial rollout *without* the env/nginx steps (backup + cleanup cron +only), when those are already handled: ```bash -just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron +just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron + deploy-logrotate ``` -### First-time `APP_KEY` on a brand-new server +### Server `APP_KEY` — idempotent, never overwrites -`deploy-env` uploads the **local** `app/.env` only when the remote has none. Do -**not** rely on a committed `.env` — generate a fresh key per environment and put -it on the server before first `deploy`: create `app/.env` locally (see -[development.md](development.md#appkey-and-the-appenv-file)) so `deploy-env` -uploads it, or create `/var/www/xamxam/.env` on the server directly: +`just provision-server` (via `scripts/provision-server-env.sh`) ensures the +server has an `APP_KEY` in `/var/www/xamxam/.env`. It is **idempotent** and will +**never overwrite an existing key**: -```bash -ssh xamxam 'sudo -u www-data bash -c "echo APP_KEY=\$(php -r \"echo base64_encode(random_bytes(32));\") > /var/www/xamxam/.env; chmod 640 /var/www/xamxam/.env; chown www-data:xamxam /var/www/xamxam/.env"' -``` +- If `/var/www/xamxam/.env` already has an `APP_KEY=…` → it is left untouched + and the script prints that the key already exists (so encrypted credentials + stay decryptable). It exits `0`. +- If the file exists but has no `APP_KEY` → a fresh key is appended, existing + lines untouched. +- If the file is absent → it is created with a fresh key. + +Ownership (`www-data:xamxam`) and permissions (`640`) are normalised after any +write. `deploy-env` also refuses to overwrite a remote `.env` that already has +`APP_KEY`. If you ever rotate `APP_KEY`, re-encrypt the SMTP password with `just reencrypt-password ` and push the new key via diff --git a/docs/development.md b/docs/development.md index f17c06f..cf4d3e9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -13,8 +13,23 @@ Setup, workflow, building assets, and testing for XAMXAM development. ## One-time setup -From the repo root, install dependencies (manually — there is no `just` recipe -for these): +From the repo root, run the provisioning script: + +```bash +just provision +``` + +This is **idempotent** and safe to re-run. It: + +1. Ensures `app/.env` has an `APP_KEY` — **never overwrites an existing key** + (it prints a message and keeps the current one so encrypted credentials stay + decryptable). +2. Installs Composer deps (`composer install`) and JS deps (`npm ci`). +3. Runs the DB schema + migrations (`just migrate`). +4. Checks the first-admin-password state and tells you if `/admin/` is still + unauthenticated. + +If you prefer to run the steps manually, or `just` isn't installed yet: ```bash composer install # PHP deps (vendor/) @@ -26,8 +41,7 @@ just migrate # create/update the SQLite DB from schema + migration — it still clones the old `php-live-reload` library and creates legacy `admin/data/` directories. Live-reload now ships inside the app (`app/public/live-reload.php`), and assets are built with rolldown/lightningcss, -not the live-reload watcher. Prefer the explicit `composer install` + `npm ci` -above. +not the live-reload watcher. Prefer `just provision`. ### APP_KEY and the app/.env file @@ -40,17 +54,19 @@ password, PeerTube credentials, and Nextcloud WebDAV sync) requires an APP_KEY= ``` -Generate one with: +Generate one manually with: ```bash php -r 'echo "APP_KEY=" . base64_encode(random_bytes(32)) . PHP_EOL;' > app/.env chmod 600 app/.env ``` -`app/.env` must **never** be committed. If the file is missing, the public site -still runs but any path that reads encrypted credentials throws -`RuntimeException: APP_KEY not found`. On a fresh clone, create it before -relying on SMTP/PeerTube/Nextcloud features. +`app/.env` must **never** be committed. `just provision` only creates `app/.env` +(or appends `APP_KEY`) when the key is absent — if the value already exists it +is left untouched so existing encrypted credentials (SMTP password, PeerTube +OAuth, Nextcloud WebDAV) remain decryptable. If the file is missing, the public +site still runs but any path that reads encrypted credentials throws +`RuntimeException: APP_KEY not found`. ### First admin login diff --git a/justfile b/justfile index f33ac87..acda458 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,13 @@ default: setup: @bash scripts/setup-dev.sh +# One-shot provisioning for a fresh clone: app/.env + APP_KEY (idempotent, never +# overwrites an existing key), composer deps, npm deps, DB schema + migrations, +# and a first-admin-password check. Prefer this over the stale `setup` recipe. +[group('dev')] +provision: + @bash scripts/provision.sh + # One-shot build of all frontend assets (run before `dev` if sources changed) [group('dev')] dev-build: @@ -484,6 +491,26 @@ deploy-migrate-storage dry_run='' target_host='xamxam': deploy-all-first: deploy deploy-backup deploy-cleanup-cron deploy-logrotate # One-shot: full initial deploy including backup and cleanup cron jobs. +# One-shot remote provisioning for a fresh xamxam server. +# Chains: server .env/APP_KEY (idempotent, never overwrites an existing key) → +# full deploy → nginx → backup + cleanup + logrotate, then prints the +# first-admin and service-credential follow-ups. +[group('deploy')] +provision-server: + @bash scripts/provision-server-env.sh xamxam + @just deploy + @just deploy-nginx + @just deploy-backup + @just deploy-cleanup-cron + @just deploy-logrotate + @echo "" + @echo "✅ Server provisioning complete." + @echo "" + @echo "Next, from the server's /admin/account:" + @echo " 1. Set the admin password (fresh DB starts unauthenticated)" + @echo " 2. Configure SMTP + PeerTube credentials (stored encrypted in the DB)" + @echo " and verify Nextcloud sync in /admin" + # ============================================================================ # Testing # ============================================================================ diff --git a/scripts/provision-server-env.sh b/scripts/provision-server-env.sh new file mode 100755 index 0000000..c00bd8e --- /dev/null +++ b/scripts/provision-server-env.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Provision (or verify) the server-side .env / APP_KEY on the xamxam host. +# +# Behavior: +# - If remote /var/www/xamxam/.env already contains APP_KEY=... → DO NOT +# overwrite it. Print a clear message and exit 0. +# - If the file exists but has no APP_KEY → append a fresh key. +# - If the file does not exist → create it with a fresh key. +# Always fixes ownership (www-data:xamxam) and perms (640) after any write. +# +# Run from local via: just provision-server +# (uses the `xamxam` SSH host as defined in ~/.ssh/config / deploy recipes) + +set -euo pipefail + +HOST="${1:-xamxam}" +ENV_PATH="/var/www/xamxam/.env" + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m' +ok() { printf "${GREEN}✓${NC} %s\n" "$*"; } +warn() { printf "${YELLOW}!${NC} %s\n" "$*"; } +info() { printf "${CYAN}→${NC} %s\n" "$*"; } +die() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; } + +info "Checking ${ENV_PATH} on ${HOST}…" + +# Does the file exist AND already have an APP_KEY value? +if ssh "$HOST" "test -f '$ENV_PATH' && grep -qE '^\s*APP_KEY=\S+' '$ENV_PATH'"; then + ok "APP_KEY already present in ${ENV_PATH} on ${HOST} — NOT overwriting it." + warn "Keeping the existing key so encrypted credentials stay decryptable." + warn "If you intend to rotate the key, use: just reencrypt-password " + exit 0 +fi + +info "APP_KEY absent — generating one on the server…" + +# Build the new key value once, server-side. +if ssh "$HOST" "command -v php >/dev/null 2>&1"; then + KEY="$(ssh "$HOST" "php -r 'echo base64_encode(random_bytes(32));'")" +else + die "php not found on ${HOST} — cannot generate APP_KEY remotely." +fi + +# Append or create, always via root (app dir is not writable by www-data). +if ssh "$HOST" "test -f '$ENV_PATH'"; then + warn "${ENV_PATH} exists but has no APP_KEY — appending (existing lines untouched)." + ssh "$HOST" "{ printf '\nAPP_KEY=%s\n' '$KEY'; } | sudo tee -a '$ENV_PATH' >/dev/null" + ok "APP_KEY appended to ${ENV_PATH}." +else + ssh "$HOST" "echo 'APP_KEY=${KEY}' | sudo tee '$ENV_PATH' >/dev/null" + info "Created ${ENV_PATH} with a new APP_KEY." +fi + +# Normalise ownership + perms regardless of which branch wrote. +ssh "$HOST" "sudo chown www-data:xamxam '$ENV_PATH' && sudo chmod 640 '$ENV_PATH'" +ok "Ownership www-data:xamxam, permissions 640." + +ok "Server APP_KEY ready." diff --git a/scripts/provision.sh b/scripts/provision.sh new file mode 100755 index 0000000..32c2b20 --- /dev/null +++ b/scripts/provision.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# One-shot provisioning for a fresh XAMXAM clone (local dev). +# +# Steps: +# 1. Ensure app/.env exists with a valid APP_KEY (NEVER overwrites an existing key). +# 2. Install dependencies: composer install + npm ci. +# 3. Create/update the SQLite DB from schema + migrations (just migrate). +# 4. Report the first-admin status (and how to set a password, if unset). +# +# Idempotent: safe to re-run on an already-provisioned clone. +# Run via: just provision + +set -euo pipefail + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m' +ok() { printf "${GREEN}✓${NC} %s\n" "$*"; } +warn() { printf "${YELLOW}!${NC} %s\n" "$*"; } +info() { printf "${CYAN}→${NC} %s\n" "$*"; } +die() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; } + +cd "$(cd "$(dirname "$0")/.." && pwd)" + +ENV_FILE="app/.env" + +# ── 1. APP_KEY / .env ────────────────────────────────────────────────────────── +info "Checking $ENV_FILE for APP_KEY…" +key_exists() { + [ -f "$ENV_FILE" ] && grep -qE '^\s*APP_KEY=\S+' "$ENV_FILE" +} + +if [ -f "$ENV_FILE" ] && key_exists; then + ok "APP_KEY already present in $ENV_FILE — NOT overwriting it." + warn "Keeping the existing key so encrypted credentials stay decryptable." +elif [ -f "$ENV_FILE" ]; then + warn "$ENV_FILE exists but has no APP_KEY — appending one (existing lines untouched)." + printf '\nAPP_KEY=%s\n' "$(php -r 'echo base64_encode(random_bytes(32));')" >> "$ENV_FILE" + chmod 600 "$ENV_FILE" + ok "APP_KEY appended to $ENV_FILE." +elif command -v php >/dev/null 2>&1; then + printf 'APP_KEY=%s\n' "$(php -r 'echo base64_encode(random_bytes(32));')" > "$ENV_FILE" + chmod 600 "$ENV_FILE" + ok "Created $ENV_FILE with a new APP_KEY." +else + warn "PHP not found — skipping automatic .env creation." + warn "Create $ENV_FILE manually with: APP_KEY=" +fi + +# ── 2. Dependencies ───────────────────────────────────────────────────────────── +info "Installing Composer dependencies…" +command -v composer >/dev/null 2>&1 || die "composer not found on PATH. Install it first." +composer install + +info "Installing npm dependencies…" +command -v npm >/dev/null 2>&1 || die "npm not found on PATH. Install it first." +npm ci + +# ── 3. Database ───────────────────────────────────────────────────────────────── +info "Creating/updating the SQLite database…" +command -v just >/dev/null 2>&1 || die "just not found on PATH. Install it first (or run scripts/migrate.sh)." +just migrate + +# ── 4. First admin password ───────────────────────────────────────────────────── +DB="app/storage/xamxam.db" +if [ -f "$DB" ] && command -v sqlite3 >/dev/null 2>&1; then + HASH_LEN="$(sqlite3 "$DB" "SELECT length(value) FROM site_settings WHERE key='admin_password_hash';" 2>/dev/null || true)" + if [ "$HASH_LEN" = "0" ] || [ -z "$HASH_LEN" ]; then + warn "No admin password is set yet — /admin/ is currently UNAUTHENTICATED (open)." + info "Set one now at http://127.0.0.1:8000/admin/account (or the production admin) to secure it." + else + ok "Admin password is already set." + fi +fi + +echo "" +printf "${GREEN}✓${NC} Provisioning complete. Start the dev server with: ${CYAN}just dev${NC}\n" +echo " Public: http://127.0.0.1:8000/ Admin: http://127.0.0.1:8000/admin/"