#!/usr/bin/env bash set -Eeuo pipefail APP_DIR="${APP_DIR:-/var/www/netgescon}" GIT_SOURCE_DIR="${GIT_SOURCE_DIR:-/var/www/netgescon-git-source}" SITE_KEY="${SITE_KEY:-}" BRANCH="${BRANCH:-main}" REPO_URL="${REPO_URL:-}" APP_URL="${APP_URL:-http://127.0.0.1}" SERVER_NAME="${SERVER_NAME:-_}" DB_NAME="${DB_NAME:-netgescon}" DB_USER="${DB_USER:-netgescon_user}" DB_PASSWORD="${DB_PASSWORD:-}" ADMIN_NAME="${ADMIN_NAME:-NetGescon Super Admin}" ADMIN_EMAIL="${ADMIN_EMAIL:-admin@netgescon.local}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-}" PHP_VERSION="${PHP_VERSION:-8.3}" RUN_NPM_BUILD=1 RUN_MIGRATIONS=1 ALLOW_EXISTING_APP_DIR=0 ALLOW_EXISTING_DATABASE=0 REPLACE_NGINX_SITE=0 usage() { cat <<'EOF' Usage: netgescon-node-bootstrap.sh [options] Required options: --repo-url Git repository URL to clone as node source --db-password MySQL password for the application user --admin-password Password for the initial super-admin user Optional options: --site-key Unique site key used for nginx/cron/supervisor names --branch Git branch to checkout (default: main) --app-dir Laravel application directory (default: /var/www/netgescon) --git-source-dir Local Git source directory for browser sync (default: /var/www/netgescon-git-source) --app-url Public APP_URL value (default: http://127.0.0.1) --server-name Nginx server_name value (default: _) --db-name MySQL database name (default: netgescon) --db-user MySQL app user (default: netgescon_user) --admin-email Initial super-admin email (default: admin@netgescon.local) --admin-name Initial super-admin display name --php-version PHP FPM version for nginx upstream (default: 8.3) --skip-build Skip npm install/build --skip-migrations Skip artisan migrate and admin bootstrap --allow-existing-app-dir Reuse an existing git worktree in the target app dir --allow-existing-database Reuse an existing database/user instead of failing --replace-nginx-site Replace an existing nginx site file for this site key --help Show this message Example: sudo bash scripts/install/netgescon-node-bootstrap.sh \ --repo-url git@your-gitea:studio/netgescon.git \ --app-url https://netgescon.example.it \ --server-name netgescon.example.it \ --db-password 'StrongDbPass123!' \ --admin-password 'StrongAdminPass123!' EOF } log() { local level="$1" shift printf '[%s] %s\n' "$level" "$*" } info() { log INFO "$@"; } warn() { log WARN "$@"; } fail() { log ERROR "$@"; exit 1; } slugify() { local value="$1" value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]') value=$(printf '%s' "$value" | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g') if [[ -z "$value" ]]; then value='netgescon' fi printf '%s' "$value" } run_root() { if [[ ${EUID} -eq 0 ]]; then "$@" else sudo "$@" fi } sql_escape() { printf '%s' "$1" | sed "s/'/''/g" } php_escape() { printf '%s' "$1" | sed "s/'/\\'/g" } mysql_database_exists() { run_root mysql -Nse "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$1'" | grep -qx "$1" } mysql_user_exists() { run_root mysql -Nse "SELECT User FROM mysql.user WHERE User = '$1' AND Host = 'localhost'" | grep -qx "$1" } set_env_value() { local key="$1" local value="$2" local file="$3" local escaped escaped=$(printf '%s' "$value" | sed -e 's/[\/&]/\\&/g') if grep -q "^${key}=" "$file"; then sed -i "s/^${key}=.*/${key}=${escaped}/" "$file" else printf '%s=%s\n' "$key" "$value" >> "$file" fi } while [[ $# -gt 0 ]]; do case "$1" in --site-key) SITE_KEY="$2" shift 2 ;; --repo-url) REPO_URL="$2" shift 2 ;; --branch) BRANCH="$2" shift 2 ;; --app-dir) APP_DIR="$2" shift 2 ;; --git-source-dir) GIT_SOURCE_DIR="$2" shift 2 ;; --app-url) APP_URL="$2" shift 2 ;; --server-name) SERVER_NAME="$2" shift 2 ;; --db-name) DB_NAME="$2" shift 2 ;; --db-user) DB_USER="$2" shift 2 ;; --db-password) DB_PASSWORD="$2" shift 2 ;; --admin-email) ADMIN_EMAIL="$2" shift 2 ;; --admin-name) ADMIN_NAME="$2" shift 2 ;; --admin-password) ADMIN_PASSWORD="$2" shift 2 ;; --php-version) PHP_VERSION="$2" shift 2 ;; --skip-build) RUN_NPM_BUILD=0 shift ;; --skip-migrations) RUN_MIGRATIONS=0 shift ;; --allow-existing-app-dir) ALLOW_EXISTING_APP_DIR=1 shift ;; --allow-existing-database) ALLOW_EXISTING_DATABASE=1 shift ;; --replace-nginx-site) REPLACE_NGINX_SITE=1 shift ;; --help|-h) usage exit 0 ;; *) fail "Unknown option: $1" ;; esac done if [[ -z "$REPO_URL" ]]; then fail "--repo-url is required" fi if [[ -z "$DB_PASSWORD" ]]; then fail "--db-password is required" fi if [[ -z "$ADMIN_PASSWORD" && $RUN_MIGRATIONS -eq 1 ]]; then fail "--admin-password is required unless --skip-migrations is used" fi for command in git php composer mysql nginx; do if ! command -v "$command" >/dev/null 2>&1; then fail "Missing required command: $command" fi done if [[ -z "$SITE_KEY" ]]; then SITE_KEY="$(slugify "$(basename "$APP_DIR")")" else SITE_KEY="$(slugify "$SITE_KEY")" fi APP_PARENT_DIR="$(dirname "$APP_DIR")" NGINX_SITE_FILE="/etc/nginx/sites-available/${SITE_KEY}.conf" NGINX_SITE_ENABLED="/etc/nginx/sites-enabled/${SITE_KEY}.conf" SUPERVISOR_FILE="/etc/supervisor/conf.d/${SITE_KEY}-worker.conf" CRON_FILE="/etc/cron.d/${SITE_KEY}-scheduler" TEMP_DIR="$(mktemp -d)" trap 'rm -rf "$TEMP_DIR"' EXIT if [[ -d "$APP_DIR" ]] && [[ -n "$(find "$APP_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]]; then if [[ $ALLOW_EXISTING_APP_DIR -ne 1 ]]; then fail "App directory already exists and is not empty: $APP_DIR. Re-run with --allow-existing-app-dir only if you really want to reuse it." fi if [[ ! -d "$APP_DIR/.git" || ! -f "$APP_DIR/artisan" ]]; then fail "Existing app directory must already be a Laravel git worktree: $APP_DIR" fi fi if [[ -d "$GIT_SOURCE_DIR" ]] && [[ ! -d "$GIT_SOURCE_DIR/.git" ]] && [[ -n "$(find "$GIT_SOURCE_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]]; then fail "Git source directory exists and is not a git repository: $GIT_SOURCE_DIR" fi if [[ -e "$NGINX_SITE_FILE" || -L "$NGINX_SITE_ENABLED" ]]; then if [[ $REPLACE_NGINX_SITE -ne 1 ]]; then fail "Nginx site already exists for ${SITE_KEY}. Re-run with --replace-nginx-site only if replacement is intentional." fi fi if mysql_database_exists "$DB_NAME"; then if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then fail "Database already exists: $DB_NAME. Re-run with --allow-existing-database only if reuse is intentional." fi fi if mysql_user_exists "$DB_USER"; then if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then fail "Database user already exists: $DB_USER. Re-run with --allow-existing-database only if reuse is intentional." fi fi info "Preparing directories" run_root install -d -o "${SUDO_USER:-$USER}" -g www-data -m 2775 "$APP_PARENT_DIR" run_root install -d -o "${SUDO_USER:-$USER}" -g www-data -m 2775 "$(dirname "$GIT_SOURCE_DIR")" if [[ -d "$GIT_SOURCE_DIR/.git" ]]; then info "Refreshing local source repository" git -C "$GIT_SOURCE_DIR" remote set-url origin "$REPO_URL" git -C "$GIT_SOURCE_DIR" fetch --all --prune git -C "$GIT_SOURCE_DIR" checkout "$BRANCH" git -C "$GIT_SOURCE_DIR" reset --hard "origin/$BRANCH" else info "Cloning local source repository" git clone --branch "$BRANCH" "$REPO_URL" "$GIT_SOURCE_DIR" fi if [[ -d "$APP_DIR/.git" ]]; then info "Refreshing application worktree" git -C "$APP_DIR" remote set-url origin "$REPO_URL" git -C "$APP_DIR" fetch --all --prune git -C "$APP_DIR" checkout "$BRANCH" git -C "$APP_DIR" reset --hard "origin/$BRANCH" else info "Cloning application worktree" git clone --branch "$BRANCH" "$GIT_SOURCE_DIR" "$APP_DIR" git -C "$APP_DIR" remote set-url origin "$REPO_URL" fi run_root git config --global --add safe.directory "$APP_DIR" run_root git config --global --add safe.directory "$GIT_SOURCE_DIR" if [[ ! -f "$APP_DIR/artisan" ]]; then fail "Laravel artisan file not found in $APP_DIR" fi info "Configuring MySQL database and user" DB_PASSWORD_ESCAPED="$(sql_escape "$DB_PASSWORD")" run_root mysql < "$TEMP_DIR/${SITE_KEY}.conf" < "$TEMP_DIR/${SITE_KEY}-scheduler" <> /var/log/${SITE_KEY}-scheduler.log 2>&1 EOF run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-scheduler" "$CRON_FILE" info "Configuring supervisor worker" cat > "$TEMP_DIR/${SITE_KEY}-worker.conf" < "$TEMP_DIR/install-node-summary.txt" <