467 lines
14 KiB
Bash
Executable File
467 lines
14 KiB
Bash
Executable File
#!/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 <url> Git repository URL to clone as node source
|
|
--db-password <password> MySQL password for the application user
|
|
--admin-password <pwd> Password for the initial super-admin user
|
|
|
|
Optional options:
|
|
--site-key <slug> Unique site key used for nginx/cron/supervisor names
|
|
--branch <name> Git branch to checkout (default: main)
|
|
--app-dir <path> Laravel application directory (default: /var/www/netgescon)
|
|
--git-source-dir <path> Local Git source directory for browser sync (default: /var/www/netgescon-git-source)
|
|
--app-url <url> Public APP_URL value (default: http://127.0.0.1)
|
|
--server-name <name> Nginx server_name value (default: _)
|
|
--db-name <name> MySQL database name (default: netgescon)
|
|
--db-user <user> MySQL app user (default: netgescon_user)
|
|
--admin-email <email> Initial super-admin email (default: admin@netgescon.local)
|
|
--admin-name <name> Initial super-admin display name
|
|
--php-version <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 <<SQL
|
|
CREATE DATABASE IF NOT EXISTS \
|
|
\\`$DB_NAME\\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
|
|
ALTER USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
|
|
GRANT ALL PRIVILEGES ON \\`$DB_NAME\\`.* TO '$DB_USER'@'localhost';
|
|
FLUSH PRIVILEGES;
|
|
SQL
|
|
|
|
info "Installing PHP dependencies"
|
|
cd "$APP_DIR"
|
|
composer install --no-dev --optimize-autoloader
|
|
|
|
if [[ $RUN_NPM_BUILD -eq 1 && -f package.json ]]; then
|
|
info "Installing frontend dependencies"
|
|
if [[ -f package-lock.json ]]; then
|
|
npm ci
|
|
else
|
|
npm install
|
|
fi
|
|
npm run build
|
|
fi
|
|
|
|
if [[ ! -f .env ]]; then
|
|
if [[ -f .env.example ]]; then
|
|
cp .env.example .env
|
|
else
|
|
fail ".env and .env.example are both missing"
|
|
fi
|
|
fi
|
|
|
|
info "Configuring Laravel environment"
|
|
set_env_value APP_NAME NetGescon .env
|
|
set_env_value APP_ENV production .env
|
|
set_env_value APP_DEBUG false .env
|
|
set_env_value APP_URL "$APP_URL" .env
|
|
set_env_value DB_CONNECTION mysql .env
|
|
set_env_value DB_HOST 127.0.0.1 .env
|
|
set_env_value DB_PORT 3306 .env
|
|
set_env_value DB_DATABASE "$DB_NAME" .env
|
|
set_env_value DB_USERNAME "$DB_USER" .env
|
|
set_env_value DB_PASSWORD "$DB_PASSWORD" .env
|
|
set_env_value CACHE_STORE file .env
|
|
set_env_value SESSION_DRIVER file .env
|
|
set_env_value QUEUE_CONNECTION database .env
|
|
set_env_value FILESYSTEM_DISK public .env
|
|
set_env_value NETGESCON_GIT_SYNC_SOURCE_REPO "$GIT_SOURCE_DIR" .env
|
|
|
|
mkdir -p storage/app/support
|
|
ln -sfn "$GIT_SOURCE_DIR" storage/app/support/git-source
|
|
|
|
php artisan key:generate --force
|
|
php artisan storage:link || true
|
|
php artisan optimize:clear
|
|
|
|
if [[ $RUN_MIGRATIONS -eq 1 ]]; then
|
|
info "Running migrations"
|
|
php artisan migrate --force
|
|
|
|
info "Creating initial super-admin user"
|
|
ADMIN_NAME_ESCAPED="$(php_escape "$ADMIN_NAME")"
|
|
ADMIN_EMAIL_ESCAPED="$(php_escape "$ADMIN_EMAIL")"
|
|
ADMIN_PASSWORD_ESCAPED="$(php_escape "$ADMIN_PASSWORD")"
|
|
|
|
php artisan tinker --execute="use App\\Models\\User; use Illuminate\\Support\\Facades\\Hash; use Spatie\\Permission\\Models\\Role; \\$role = Role::firstOrCreate(['name' => 'super-admin', 'guard_name' => 'web']); \\$adminRole = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']); \\$user = User::updateOrCreate(['email' => '$ADMIN_EMAIL_ESCAPED'], ['name' => '$ADMIN_NAME_ESCAPED', 'email_verified_at' => now(), 'password' => Hash::make('$ADMIN_PASSWORD_ESCAPED'), 'is_active' => true, 'registration_status' => 'approved']); \\$user->syncRoles(['super-admin', 'admin']);"
|
|
fi
|
|
|
|
info "Caching Laravel configuration"
|
|
php artisan config:cache
|
|
php artisan route:cache
|
|
php artisan view:cache
|
|
|
|
info "Setting permissions"
|
|
run_root chown -R www-data:www-data "$APP_DIR"
|
|
run_root chmod -R 775 "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
|
|
|
|
info "Writing nginx virtual host"
|
|
cat > "$TEMP_DIR/${SITE_KEY}.conf" <<EOF
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name $SERVER_NAME;
|
|
root $APP_DIR/public;
|
|
|
|
index index.php index.html;
|
|
client_max_body_size 64m;
|
|
|
|
access_log /var/log/nginx/${SITE_KEY}-access.log;
|
|
error_log /var/log/nginx/${SITE_KEY}-error.log;
|
|
|
|
location / {
|
|
try_files \\$uri \\$uri/ /index.php?\$query_string;
|
|
}
|
|
|
|
location ~ \\.php$ {
|
|
include snippets/fastcgi-php.conf;
|
|
fastcgi_pass unix:/run/php/php${PHP_VERSION}-fpm.sock;
|
|
fastcgi_param SCRIPT_FILENAME \\$realpath_root\$fastcgi_script_name;
|
|
include fastcgi_params;
|
|
}
|
|
|
|
location ~ /\\.(?!well-known).* {
|
|
deny all;
|
|
}
|
|
}
|
|
EOF
|
|
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}.conf" "$NGINX_SITE_FILE"
|
|
run_root ln -sfn "$NGINX_SITE_FILE" "$NGINX_SITE_ENABLED"
|
|
run_root nginx -t
|
|
run_root systemctl reload nginx
|
|
|
|
info "Configuring scheduler cron"
|
|
cat > "$TEMP_DIR/${SITE_KEY}-scheduler" <<EOF
|
|
* * * * * www-data cd $APP_DIR && php artisan schedule:run >> /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" <<EOF
|
|
[program:${SITE_KEY}-worker]
|
|
command=php $APP_DIR/artisan queue:work --sleep=3 --tries=3 --max-time=3600
|
|
directory=$APP_DIR
|
|
autostart=true
|
|
autorestart=true
|
|
user=www-data
|
|
redirect_stderr=true
|
|
stdout_logfile=/var/log/${SITE_KEY}-worker.log
|
|
stopwaitsecs=3600
|
|
EOF
|
|
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-worker.conf" "$SUPERVISOR_FILE"
|
|
run_root supervisorctl reread
|
|
run_root supervisorctl update
|
|
run_root supervisorctl restart "${SITE_KEY}-worker" || true
|
|
|
|
SUMMARY_FILE="$APP_DIR/install-node-summary.txt"
|
|
cat > "$TEMP_DIR/install-node-summary.txt" <<EOF
|
|
NetGescon node bootstrap completed
|
|
Date: $(date -Is)
|
|
Site key: $SITE_KEY
|
|
Application dir: $APP_DIR
|
|
Git source dir: $GIT_SOURCE_DIR
|
|
Branch: $BRANCH
|
|
APP_URL: $APP_URL
|
|
Server name: $SERVER_NAME
|
|
Database: $DB_NAME
|
|
Database user: $DB_USER
|
|
Admin email: $ADMIN_EMAIL
|
|
Update launcher: $APP_URL/admin-filament/supporto/aggiornamento-nodo
|
|
EOF
|
|
run_root install -o www-data -g www-data -m 0644 "$TEMP_DIR/install-node-summary.txt" "$SUMMARY_FILE"
|
|
|
|
info "NetGescon node bootstrap completed"
|
|
printf '\nOpen after first login:\n'
|
|
printf ' %s/admin-filament/supporto/aggiornamento-nodo\n' "$APP_URL"
|
|
printf '\nSummary file:\n'
|
|
printf ' %s\n' "$SUMMARY_FILE"
|