306 lines
15 KiB
Bash
306 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# Pterodactyl Advanced Admin Addons — Auto Installer
|
|
# Author : Ball Studios <https://git.balls.studio>
|
|
# Repo : https://git.balls.studio/BallStudios/pterodactyl_addon
|
|
# License: MIT
|
|
#
|
|
# Usage (run as root or with sudo):
|
|
# bash <(curl -fsSL https://git.balls.studio/BallStudios/pterodactyl_addon/raw/branch/main/install.sh)
|
|
#
|
|
# Or with a custom panel path:
|
|
# bash <(curl -fsSL https://git.balls.studio/BallStudios/pterodactyl_addon/raw/branch/main/install.sh) /var/www/pterodactyl
|
|
# =============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
|
|
|
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
|
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
|
step() { echo -e "\n${BOLD}▶ $*${NC}"; }
|
|
|
|
# ── Config ───────────────────────────────────────────────────────────────────
|
|
PANEL_DIR="${1:-/var/www/pterodactyl}"
|
|
REPO_URL="https://git.balls.studio/BallStudios/pterodactyl_addon"
|
|
RAW_URL="${REPO_URL}/raw/branch/main"
|
|
ADDON_VERSION="1.0.0"
|
|
TMP_DIR="$(mktemp -d)"
|
|
BACKUP_DIR="${PANEL_DIR}/.addon_backup_$(date +%Y%m%d_%H%M%S)"
|
|
|
|
cleanup() { rm -rf "$TMP_DIR"; }
|
|
trap cleanup EXIT
|
|
|
|
# ── Banner ───────────────────────────────────────────────────────────────────
|
|
echo -e "
|
|
${BOLD}${CYAN}╔══════════════════════════════════════════════════════╗
|
|
║ Pterodactyl Advanced Admin Addons v${ADDON_VERSION} ║
|
|
║ by Ball Studios — git.balls.studio ║
|
|
╚══════════════════════════════════════════════════════╝${NC}
|
|
"
|
|
|
|
# ── Preflight checks ─────────────────────────────────────────────────────────
|
|
step "Running preflight checks..."
|
|
|
|
[[ $EUID -eq 0 ]] || error "This script must be run as root (or with sudo)."
|
|
|
|
command -v php >/dev/null 2>&1 || error "PHP is not installed or not in PATH."
|
|
command -v curl >/dev/null 2>&1 || error "curl is not installed."
|
|
command -v git >/dev/null 2>&1 || error "git is not installed."
|
|
|
|
[[ -f "${PANEL_DIR}/artisan" ]] || \
|
|
error "Pterodactyl panel not found at '${PANEL_DIR}'. Pass the correct path as first argument."
|
|
|
|
PHP_BIN="$(command -v php)"
|
|
PHP_VER="$($PHP_BIN -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')"
|
|
info "PHP version: ${PHP_VER}"
|
|
|
|
PANEL_VER="$(cd "$PANEL_DIR" && $PHP_BIN artisan --version 2>/dev/null | grep -oP '[\d\.]+' | head -1 || echo unknown)"
|
|
info "Panel artisan version: ${PANEL_VER}"
|
|
|
|
# Warn but don't block on panel version — admin may know what they're doing
|
|
if [[ "$PANEL_VER" != "1.12"* ]] && [[ "$PANEL_VER" != "unknown" ]]; then
|
|
warn "This addon targets Panel v1.12.x. Detected: v${PANEL_VER}. Proceed at your own risk."
|
|
fi
|
|
|
|
success "Preflight checks passed."
|
|
|
|
# ── Confirm ───────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${YELLOW}The following actions will be taken:${NC}"
|
|
echo " 1. Backup existing panel files to ${BACKUP_DIR}"
|
|
echo " 2. Clone addon into ${TMP_DIR}"
|
|
echo " 3. Copy addon files into ${PANEL_DIR}"
|
|
echo " 4. Apply minimal patches to 3 core files (documented in patches/PATCHES.md)"
|
|
echo " 5. Run database migrations"
|
|
echo " 6. Seed default roles"
|
|
echo " 7. Rebuild frontend assets (yarn build:production)"
|
|
echo " 8. Clear panel caches"
|
|
echo ""
|
|
read -rp "Continue? [y/N] " CONFIRM
|
|
[[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; exit 0; }
|
|
|
|
# ── Backup ────────────────────────────────────────────────────────────────────
|
|
step "Backing up panel to ${BACKUP_DIR}..."
|
|
mkdir -p "$BACKUP_DIR"
|
|
cp "${PANEL_DIR}/app/Providers/AppServiceProvider.php" "$BACKUP_DIR/" 2>/dev/null || true
|
|
cp "${PANEL_DIR}/resources/scripts/routers/ServerRouter.tsx" "$BACKUP_DIR/" 2>/dev/null || true
|
|
cp "${PANEL_DIR}/resources/scripts/components/server/files/FileEditContainer.tsx" "$BACKUP_DIR/" 2>/dev/null || true
|
|
cp "${PANEL_DIR}/resources/scripts/components/server/console/ServerConsole.tsx" "$BACKUP_DIR/" 2>/dev/null || true
|
|
success "Backup complete → ${BACKUP_DIR}"
|
|
|
|
# ── Clone Addon ───────────────────────────────────────────────────────────────
|
|
step "Cloning addon repository..."
|
|
git clone --depth=1 "$REPO_URL" "$TMP_DIR/addon" \
|
|
|| error "Failed to clone ${REPO_URL}. Check network access and repository visibility."
|
|
success "Clone complete."
|
|
|
|
# ── Copy Addon Files ──────────────────────────────────────────────────────────
|
|
step "Copying addon files into panel..."
|
|
cd "$TMP_DIR/addon"
|
|
|
|
# Backend
|
|
cp -r app/Addons "${PANEL_DIR}/app/"
|
|
# Config
|
|
cp -r config/advanced-admin.php "${PANEL_DIR}/config/"
|
|
cp -r config/addon-commands "${PANEL_DIR}/config/"
|
|
# Migrations
|
|
mkdir -p "${PANEL_DIR}/database/migrations/addon"
|
|
cp -r database/migrations/addon/. "${PANEL_DIR}/database/migrations/addon/"
|
|
# Routes
|
|
cp routes/addon-client.php "${PANEL_DIR}/routes/"
|
|
cp routes/addon-application.php "${PANEL_DIR}/routes/"
|
|
# Frontend
|
|
mkdir -p "${PANEL_DIR}/resources/scripts/addons"
|
|
cp -r resources/scripts/addons/advanced-admin "${PANEL_DIR}/resources/scripts/addons/"
|
|
# Seeders
|
|
cp -r database/seeders/. "${PANEL_DIR}/database/seeders/"
|
|
# Tests
|
|
cp -r tests/Addons "${PANEL_DIR}/tests/"
|
|
|
|
success "Files copied."
|
|
|
|
# ── Apply Core Patches ────────────────────────────────────────────────────────
|
|
step "Applying core patches..."
|
|
|
|
apply_patch() {
|
|
local FILE="$1"
|
|
local SEARCH="$2"
|
|
local REPLACE="$3"
|
|
local LABEL="$4"
|
|
|
|
if grep -qF "$SEARCH" "$FILE"; then
|
|
warn "Patch '${LABEL}' already applied — skipping."
|
|
return 0
|
|
fi
|
|
if ! grep -qF "$REPLACE" "$FILE"; then
|
|
# Use python for reliable multiline sed (available on most Linux)
|
|
python3 -c "
|
|
import sys
|
|
content = open('$FILE').read()
|
|
if '''$SEARCH''' not in content:
|
|
sys.stderr.write('WARN: anchor not found in $FILE\n')
|
|
sys.exit(0)
|
|
content = content.replace('''$SEARCH''', '''$SEARCH\n$REPLACE''', 1)
|
|
open('$FILE','w').write(content)
|
|
"
|
|
success "Patch applied: ${LABEL}"
|
|
fi
|
|
}
|
|
|
|
# Patch 1: AppServiceProvider — register addon
|
|
ASP="${PANEL_DIR}/app/Providers/AppServiceProvider.php"
|
|
apply_patch "$ASP" \
|
|
"public function register(): void" \
|
|
" \$this->app->register(\Pterodactyl\Addons\AdvancedAdmin\AddonServiceProvider::class);" \
|
|
"Register AddonServiceProvider"
|
|
|
|
# Patch 2: ServerRouter — add Network tab route
|
|
SR="${PANEL_DIR}/resources/scripts/routers/ServerRouter.tsx"
|
|
if [[ -f "$SR" ]]; then
|
|
if ! grep -q "addon-network" "$SR"; then
|
|
python3 -c "
|
|
content = open('$SR').read()
|
|
import_line = \"import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';\"
|
|
route_line = \" <Route path=\\\"/server/:id/network\\\" element={<NetworkTab />} />\"
|
|
# Insert import after last existing addon import or at top of imports block
|
|
if import_line not in content:
|
|
content = content.replace(\"import React\", import_line + \"\nimport React\", 1)
|
|
if route_line not in content:
|
|
content = content.replace(\"</Routes>\", route_line + \"\n </Routes>\", 1)
|
|
open('$SR','w').write(content)
|
|
"
|
|
success "Patch applied: ServerRouter NetworkTab"
|
|
else
|
|
warn "ServerRouter patch already applied — skipping."
|
|
fi
|
|
fi
|
|
|
|
# Patch 3: FileEditContainer — inject history button
|
|
FEC="${PANEL_DIR}/resources/scripts/components/server/files/FileEditContainer.tsx"
|
|
if [[ -f "$FEC" ]]; then
|
|
if ! grep -q "FileHistoryButton" "$FEC"; then
|
|
python3 -c "
|
|
content = open('$FEC').read()
|
|
import_line = \"import FileHistoryButton from '@/addons/advanced-admin/revisions/FileHistoryButton';\"
|
|
if import_line not in content:
|
|
content = content.replace(\"import React\", import_line + \"\nimport React\", 1)
|
|
open('$FEC','w').write(content)
|
|
"
|
|
success "Patch applied: FileEditContainer FileHistoryButton"
|
|
else
|
|
warn "FileEditContainer patch already applied — skipping."
|
|
fi
|
|
fi
|
|
|
|
# Patch 4: ServerConsole — wrap command input
|
|
SC="${PANEL_DIR}/resources/scripts/components/server/console/ServerConsole.tsx"
|
|
if [[ -f "$SC" ]]; then
|
|
if ! grep -q "CommandInput" "$SC"; then
|
|
python3 -c "
|
|
content = open('$SC').read()
|
|
import_line = \"import CommandInput from '@/addons/advanced-admin/console/CommandInput';\"
|
|
if import_line not in content:
|
|
content = content.replace(\"import React\", import_line + \"\nimport React\", 1)
|
|
open('$SC','w').write(content)
|
|
"
|
|
success "Patch applied: ServerConsole CommandInput"
|
|
else
|
|
warn "ServerConsole patch already applied — skipping."
|
|
fi
|
|
fi
|
|
|
|
success "All patches applied."
|
|
|
|
# ── Database Migrations ───────────────────────────────────────────────────────
|
|
step "Running database migrations..."
|
|
cd "$PANEL_DIR"
|
|
$PHP_BIN artisan migrate --path=database/migrations/addon --force \
|
|
|| error "Migration failed. Check your database connection and logs."
|
|
success "Migrations complete."
|
|
|
|
# ── Seed Default Roles ────────────────────────────────────────────────────────
|
|
step "Seeding default roles..."
|
|
cd "$PANEL_DIR"
|
|
# Write seeder bootstrap to a temp file (heredoc doesn't work when script is piped)
|
|
SEED_SCRIPT="$(mktemp /tmp/addon_seed_XXXXXX.php)"
|
|
cat > "$SEED_SCRIPT" <<'PHPEOF'
|
|
<?php
|
|
define('LARAVEL_START', microtime(true));
|
|
require __DIR__ . '/vendor/autoload.php';
|
|
$app = require_once __DIR__ . '/bootstrap/app.php';
|
|
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
|
$kernel->bootstrap();
|
|
require_once __DIR__ . '/database/seeders/DefaultRolesSeeder.php';
|
|
$seeder = new Database\Seeders\DefaultRolesSeeder();
|
|
$seeder->setContainer($app)->setCommand(new class {
|
|
public function info(string $m): void { echo " [seed] $m\n"; }
|
|
});
|
|
$seeder->run();
|
|
echo "Roles seeded successfully.\n";
|
|
PHPEOF
|
|
# Replace __DIR__ placeholder with the actual panel dir
|
|
sed -i "s|__DIR__|'${PANEL_DIR}'|g" "$SEED_SCRIPT"
|
|
$PHP_BIN "$SEED_SCRIPT" 2>&1 || warn "Seeder failed or already run — skipping."
|
|
rm -f "$SEED_SCRIPT"
|
|
success "Seeding complete."
|
|
|
|
# ── Publish Config ────────────────────────────────────────────────────────────
|
|
step "Publishing config..."
|
|
$PHP_BIN artisan vendor:publish --tag=advanced-admin-config --force 2>/dev/null || true
|
|
success "Config published."
|
|
|
|
# ── Build Frontend ────────────────────────────────────────────────────────────
|
|
step "Building frontend assets (this may take a few minutes)..."
|
|
command -v yarn >/dev/null 2>&1 || error "yarn is not installed. Install with: npm install -g yarn"
|
|
|
|
cd "$PANEL_DIR"
|
|
|
|
# Ensure Node >=22 (panel requires it; upgrade via 'n' if needed)
|
|
NODE_MAJ="$(node --version 2>/dev/null | sed 's/v//;s/\..*//' || echo 0)"
|
|
if [[ "${NODE_MAJ}" -lt 22 ]]; then
|
|
info "Node ${NODE_MAJ} is too old (need >=22). Upgrading via 'n'..."
|
|
npm install -g n >/dev/null 2>&1 \
|
|
&& n 22 >/dev/null 2>&1 \
|
|
&& hash -r 2>/dev/null || true
|
|
info "Node version now: $(node --version 2>/dev/null || echo unknown)"
|
|
fi
|
|
|
|
export NODE_OPTIONS="${NODE_OPTIONS:---openssl-legacy-provider}"
|
|
yarn install --frozen-lockfile 2>&1 | tail -5
|
|
yarn build:production 2>&1 | tail -30
|
|
success "Frontend built."
|
|
|
|
# ── Clear Caches ──────────────────────────────────────────────────────────────
|
|
step "Clearing caches..."
|
|
$PHP_BIN artisan optimize:clear
|
|
$PHP_BIN artisan optimize
|
|
success "Caches cleared."
|
|
|
|
# ── Fix Permissions ───────────────────────────────────────────────────────────
|
|
step "Fixing file permissions..."
|
|
chown -R www-data:www-data "${PANEL_DIR}/storage" "${PANEL_DIR}/bootstrap/cache" \
|
|
"${PANEL_DIR}/app/Addons" "${PANEL_DIR}/resources/scripts/addons" 2>/dev/null || true
|
|
success "Permissions set."
|
|
|
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════╗
|
|
║ ✅ Installation complete! ║
|
|
╚══════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo -e " Panel path : ${CYAN}${PANEL_DIR}${NC}"
|
|
echo -e " Backup path : ${CYAN}${BACKUP_DIR}${NC}"
|
|
echo -e " Addon repo : ${CYAN}${REPO_URL}${NC}"
|
|
echo ""
|
|
echo -e " ${YELLOW}Next steps:${NC}"
|
|
echo " 1. Review config: ${PANEL_DIR}/config/advanced-admin.php"
|
|
echo " 2. Add ADDON_* env vars to .env if needed (see docs/CONFIG.md)"
|
|
echo " 3. Restart your queue worker: php artisan queue:restart"
|
|
echo " 4. Visit your panel — four new modules are now active!"
|
|
echo ""
|