Compare commits
7 Commits
ded0d6f1b1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| acdb9fd9c2 | |||
| 73a84d762f | |||
| f5f0eee857 | |||
| e6111e0a0e | |||
| 3830e54988 | |||
| 3bb4b323e7 | |||
| 3236acab16 |
@@ -23,6 +23,11 @@ class AutocompleteController extends Controller
|
|||||||
{
|
{
|
||||||
$this->requireAccess($request, $server);
|
$this->requireAccess($request, $server);
|
||||||
|
|
||||||
|
// Pterodactyl's route model binding resolves by UUID; ensure we have the integer PK
|
||||||
|
if ($server->id === null) {
|
||||||
|
$server = Server::where('uuid', $server->uuid)->firstOrFail();
|
||||||
|
}
|
||||||
|
|
||||||
$query = (string) $request->get('q', '');
|
$query = (string) $request->get('q', '');
|
||||||
$suggestions = $this->engine->suggest(
|
$suggestions = $this->engine->suggest(
|
||||||
$server->id,
|
$server->id,
|
||||||
|
|||||||
@@ -85,17 +85,62 @@ class ServerRoleController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** GET /api/client/servers/{server}/addons/permissions/effective/{userId} */
|
/** GET /api/client/servers/{server}/addons/permissions/effective/{userId} */
|
||||||
public function effectivePermissions(Request $request, Server $server, int $targetUserId): JsonResponse
|
public function effectivePermissions(Request $request, Server $server, string $targetUserId): JsonResponse
|
||||||
{
|
{
|
||||||
$this->authorizeServerAdmin($request, $server);
|
// Allow callers to pass "me" to resolve to their own user ID
|
||||||
|
if ($targetUserId === 'me') {
|
||||||
|
$targetUserId = (string) $request->user()?->id;
|
||||||
|
}
|
||||||
|
|
||||||
$effective = $this->resolver->resolveAll(
|
// Non-admins can only query their own permissions
|
||||||
$targetUserId,
|
$user = $request->user();
|
||||||
$server->id,
|
$isAdmin = $user && ($user->root_admin || $server->owner_id === $user->id);
|
||||||
RoleService::KNOWN_PERMISSIONS
|
if (!$isAdmin && (string) $user?->id !== $targetUserId) {
|
||||||
);
|
abort(403, 'You may only query your own permissions.');
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json(['data' => $effective]);
|
// If target user is the owner, return an Owner role item
|
||||||
|
if ((int) $targetUserId === $server->owner_id) {
|
||||||
|
$ownerRole = [
|
||||||
|
'role' => [
|
||||||
|
'id' => 0,
|
||||||
|
'name' => 'Owner',
|
||||||
|
'color' => '#f59e0b',
|
||||||
|
'description' => 'Full server access — assigned to the server owner',
|
||||||
|
'permissions' => array_keys(RoleService::KNOWN_PERMISSIONS),
|
||||||
|
'created_at' => now()->toIso8601String(),
|
||||||
|
],
|
||||||
|
'source' => 'Server Owner',
|
||||||
|
];
|
||||||
|
return response()->json(['data' => [$ownerRole]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's assigned roles on this server
|
||||||
|
$roles = $this->resolver->getUserRolesForServer((int) $targetUserId, $server->id);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
foreach ($roles as $role) {
|
||||||
|
$allowedPermissions = [];
|
||||||
|
foreach ($role->permissions as $p) {
|
||||||
|
if ($p->value === 'allow') {
|
||||||
|
$allowedPermissions[] = $p->permission;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[] = [
|
||||||
|
'role' => [
|
||||||
|
'id' => $role->id,
|
||||||
|
'name' => $role->name,
|
||||||
|
'color' => $role->color,
|
||||||
|
'description' => $role->description,
|
||||||
|
'permissions' => $allowedPermissions,
|
||||||
|
'created_at' => $role->created_at?->toIso8601String(),
|
||||||
|
],
|
||||||
|
'source' => 'Role Assignment',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['data' => $data]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PUT /api/client/servers/{server}/addons/permissions/override */
|
/** PUT /api/client/servers/{server}/addons/permissions/override */
|
||||||
|
|||||||
130
app/Addons/AdvancedAdmin/Models/CommandDefinition.php
Normal file
130
app/Addons/AdvancedAdmin/Models/CommandDefinition.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Addons\AdvancedAdmin\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class CommandDefinition extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'addon_command_definitions';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'server_id',
|
||||||
|
'node_id',
|
||||||
|
'egg_id',
|
||||||
|
'plugin_source',
|
||||||
|
'command_name',
|
||||||
|
'definition',
|
||||||
|
'is_active',
|
||||||
|
// Virtual/fillable fields for controller convenience
|
||||||
|
'command',
|
||||||
|
'description',
|
||||||
|
'syntax',
|
||||||
|
'dangerous',
|
||||||
|
'plugin',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'definition' => 'array',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'dangerous' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'command',
|
||||||
|
'description',
|
||||||
|
'syntax',
|
||||||
|
'dangerous',
|
||||||
|
'plugin',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static function booted()
|
||||||
|
{
|
||||||
|
// Add global scope to alias command_name as command for ordering
|
||||||
|
static::addGlobalScope('alias_command', function ($builder) {
|
||||||
|
$builder->select('addon_command_definitions.*')
|
||||||
|
->selectRaw('addon_command_definitions.command_name as command');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync helper fields to database columns on saving
|
||||||
|
static::saving(function (CommandDefinition $model) {
|
||||||
|
if ($model->plugin) {
|
||||||
|
$model->plugin_source = $model->plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($model->command) {
|
||||||
|
$model->command_name = $model->command;
|
||||||
|
}
|
||||||
|
|
||||||
|
$def = $model->definition ?? [];
|
||||||
|
$def['name'] = $model->command_name ?? $def['name'] ?? '';
|
||||||
|
$def['description'] = $model->description ?? $def['description'] ?? '';
|
||||||
|
$def['syntax'] = $model->syntax ?? $def['syntax'] ?? '';
|
||||||
|
$def['dangerous'] = (bool) ($model->dangerous ?? $def['dangerous'] ?? false);
|
||||||
|
$def['plugin'] = $model->plugin_source ?? $def['plugin'] ?? 'custom';
|
||||||
|
$def['args'] = $def['args'] ?? [];
|
||||||
|
|
||||||
|
$model->definition = $def;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessors and Mutators for virtual fields
|
||||||
|
|
||||||
|
public function getCommandAttribute()
|
||||||
|
{
|
||||||
|
return $this->command_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCommandAttribute($value)
|
||||||
|
{
|
||||||
|
$this->command_name = $value;
|
||||||
|
$this->attributes['command_name'] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescriptionAttribute()
|
||||||
|
{
|
||||||
|
return $this->definition['description'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescriptionAttribute($value)
|
||||||
|
{
|
||||||
|
$def = $this->definition ?? [];
|
||||||
|
$def['description'] = $value;
|
||||||
|
$this->definition = $def;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSyntaxAttribute()
|
||||||
|
{
|
||||||
|
return $this->definition['syntax'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSyntaxAttribute($value)
|
||||||
|
{
|
||||||
|
$def = $this->definition ?? [];
|
||||||
|
$def['syntax'] = $value;
|
||||||
|
$this->definition = $def;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDangerousAttribute()
|
||||||
|
{
|
||||||
|
return (bool) ($this->definition['dangerous'] ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDangerousAttribute($value)
|
||||||
|
{
|
||||||
|
$def = $this->definition ?? [];
|
||||||
|
$def['dangerous'] = (bool) $value;
|
||||||
|
$this->definition = $def;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPluginAttribute()
|
||||||
|
{
|
||||||
|
return $this->plugin_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPluginAttribute($value)
|
||||||
|
{
|
||||||
|
$this->plugin_source = $value;
|
||||||
|
$this->attributes['plugin_source'] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Addons/AdvancedAdmin/Models/ConsolePlayerCache.php
Normal file
25
app/Addons/AdvancedAdmin/Models/ConsolePlayerCache.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Addons\AdvancedAdmin\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ConsolePlayerCache extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'addon_console_player_cache';
|
||||||
|
|
||||||
|
protected $primaryKey = null;
|
||||||
|
public $incrementing = false;
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'server_id',
|
||||||
|
'player_name',
|
||||||
|
'last_seen_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'server_id' => 'integer',
|
||||||
|
'last_seen_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -103,7 +103,11 @@ class DefaultRolesSeeder extends Seeder
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->command->info("Created default role: {$roleData['name']}");
|
if ($this->command) {
|
||||||
|
$this->command->info("Created default role: {$roleData['name']}");
|
||||||
|
} else {
|
||||||
|
echo "Created default role: {$roleData['name']}\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
683
install.sh
683
install.sh
@@ -1,20 +1,23 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Pterodactyl Advanced Admin Addons — Auto Installer
|
# Pterodactyl Advanced Admin Addons — Installer / Updater
|
||||||
# Author : Ball Studios <https://git.balls.studio>
|
# Author : Ball Studios <https://git.balls.studio>
|
||||||
# Repo : https://git.balls.studio/BallStudios/pterodactyl_addon
|
# Repo : https://git.balls.studio/BallStudios/pterodactyl_addon
|
||||||
# License: MIT
|
# License: MIT
|
||||||
#
|
#
|
||||||
# Usage (run as root or with sudo):
|
# Fresh install:
|
||||||
# bash <(curl -fsSL https://git.balls.studio/BallStudios/pterodactyl_addon/raw/branch/main/install.sh)
|
# bash <(curl -fsSL https://git.balls.studio/BallStudios/pterodactyl_addon/raw/branch/main/install.sh)
|
||||||
#
|
#
|
||||||
# Or with a custom panel path:
|
# Update existing install:
|
||||||
# bash <(curl -fsSL https://git.balls.studio/BallStudios/pterodactyl_addon/raw/branch/main/install.sh) /var/www/pterodactyl
|
# bash <(curl -fsSL .../install.sh) --update
|
||||||
|
#
|
||||||
|
# Custom panel path:
|
||||||
|
# bash <(curl -fsSL .../install.sh) /var/www/pterodactyl
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
# ── Colours ───────────────────────────────────────────────────────────────────
|
||||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
|
||||||
@@ -23,287 +26,537 @@ success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
|||||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||||
step() { echo -e "\n${BOLD}▶ $*${NC}"; }
|
step() { echo -e "\n${BOLD}▶ $*${NC}"; }
|
||||||
|
check() { echo -e " ${GREEN}✔${NC} $*"; }
|
||||||
|
fail() { echo -e " ${RED}✘${NC} $*"; }
|
||||||
|
|
||||||
# ── Config ───────────────────────────────────────────────────────────────────
|
# ── Args ──────────────────────────────────────────────────────────────────────
|
||||||
PANEL_DIR="${1:-/var/www/pterodactyl}"
|
PANEL_DIR="/var/www/pterodactyl"
|
||||||
|
IS_UPDATE=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--update|-u) IS_UPDATE=true ;;
|
||||||
|
/*) PANEL_DIR="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Config ────────────────────────────────────────────────────────────────────
|
||||||
REPO_URL="https://git.balls.studio/BallStudios/pterodactyl_addon"
|
REPO_URL="https://git.balls.studio/BallStudios/pterodactyl_addon"
|
||||||
RAW_URL="${REPO_URL}/raw/branch/main"
|
RAW_URL="${REPO_URL}/raw/branch/main"
|
||||||
ADDON_VERSION="1.0.0"
|
ADDON_VERSION="1.0.0"
|
||||||
TMP_DIR="$(mktemp -d)"
|
TMP_DIR="$(mktemp -d)"
|
||||||
BACKUP_DIR="${PANEL_DIR}/.addon_backup_$(date +%Y%m%d_%H%M%S)"
|
BACKUP_DIR="${PANEL_DIR}/.addon_backup_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
ADDON_MARKER="${PANEL_DIR}/.addon_installed"
|
||||||
|
|
||||||
cleanup() { rm -rf "$TMP_DIR"; }
|
cleanup() { rm -rf "$TMP_DIR"; }
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
# ── Banner ───────────────────────────────────────────────────────────────────
|
# ── Banner ────────────────────────────────────────────────────────────────────
|
||||||
echo -e "
|
echo -e "
|
||||||
${BOLD}${CYAN}╔══════════════════════════════════════════════════════╗
|
${BOLD}${CYAN}╔══════════════════════════════════════════════════════════╗
|
||||||
║ Pterodactyl Advanced Admin Addons v${ADDON_VERSION} ║
|
║ Pterodactyl Advanced Admin Addons v${ADDON_VERSION} ║
|
||||||
║ by Ball Studios — git.balls.studio ║
|
║ by Ball Studios — git.balls.studio ║
|
||||||
╚══════════════════════════════════════════════════════╝${NC}
|
╚══════════════════════════════════════════════════════════╝${NC}
|
||||||
"
|
"
|
||||||
|
|
||||||
# ── Preflight checks ─────────────────────────────────────────────────────────
|
if [[ -f "$ADDON_MARKER" ]] && [[ "$IS_UPDATE" == "false" ]]; then
|
||||||
step "Running preflight checks..."
|
INSTALLED_VER="$(cat "$ADDON_MARKER" 2>/dev/null || echo unknown)"
|
||||||
|
echo -e "${YELLOW}[!] Addon v${INSTALLED_VER} is already installed.${NC}"
|
||||||
[[ $EUID -eq 0 ]] || error "This script must be run as root (or with sudo)."
|
echo -e " Run with ${CYAN}--update${NC} to update, or re-run without to reinstall."
|
||||||
|
echo ""
|
||||||
command -v php >/dev/null 2>&1 || error "PHP is not installed or not in PATH."
|
read -rp "Reinstall/update anyway? [y/N] " CONFIRM
|
||||||
command -v curl >/dev/null 2>&1 || error "curl is not installed."
|
[[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; exit 0; }
|
||||||
command -v git >/dev/null 2>&1 || error "git is not installed."
|
IS_UPDATE=true
|
||||||
|
|
||||||
[[ -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
|
fi
|
||||||
|
|
||||||
success "Preflight checks passed."
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# STEP 1: DEPENDENCY CHECKS
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
step "Checking dependencies..."
|
||||||
|
|
||||||
|
DEPS_OK=true
|
||||||
|
|
||||||
|
# Root check
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
fail "Must run as root (or with sudo)."
|
||||||
|
DEPS_OK=false
|
||||||
|
else
|
||||||
|
check "Running as root"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Panel directory
|
||||||
|
if [[ -f "${PANEL_DIR}/artisan" ]]; then
|
||||||
|
check "Pterodactyl panel found at ${PANEL_DIR}"
|
||||||
|
else
|
||||||
|
fail "Panel not found at ${PANEL_DIR}"
|
||||||
|
DEPS_OK=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Blueprint check (warn if installed — may conflict)
|
||||||
|
if [[ -f "${PANEL_DIR}/BlueprintFramework" ]] || \
|
||||||
|
[[ -d "${PANEL_DIR}/blueprint" ]] || \
|
||||||
|
command -v blueprint >/dev/null 2>&1; then
|
||||||
|
warn "Blueprint Framework detected. This addon patches core files directly"
|
||||||
|
warn "and may conflict with Blueprint extensions. Proceed with caution."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PHP
|
||||||
|
if command -v php >/dev/null 2>&1; then
|
||||||
|
PHP_BIN="$(command -v php)"
|
||||||
|
PHP_VER="$($PHP_BIN -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;' 2>/dev/null || echo 0)"
|
||||||
|
PHP_MAJOR="${PHP_VER%%.*}"
|
||||||
|
if [[ "$PHP_MAJOR" -ge 8 ]]; then
|
||||||
|
check "PHP ${PHP_VER}"
|
||||||
|
else
|
||||||
|
fail "PHP ${PHP_VER} is too old (need >= 8.0)"
|
||||||
|
DEPS_OK=false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "PHP not found"
|
||||||
|
DEPS_OK=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Required PHP extensions
|
||||||
|
for EXT in pdo_mysql mbstring openssl tokenizer xml ctype json bcmath; do
|
||||||
|
if $PHP_BIN -m 2>/dev/null | grep -qi "^${EXT}$"; then
|
||||||
|
check "PHP ext: ${EXT}"
|
||||||
|
else
|
||||||
|
# Try to install automatically
|
||||||
|
PKG="php${PHP_VER}-${EXT//_/-}"
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
info "Installing missing PHP extension: ${EXT}..."
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y "$PKG" -q >/dev/null 2>&1 \
|
||||||
|
&& check "PHP ext: ${EXT} (auto-installed)" \
|
||||||
|
|| warn "PHP ext: ${EXT} missing — install manually: apt install ${PKG}"
|
||||||
|
else
|
||||||
|
warn "PHP ext: ${EXT} missing — install it manually"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Composer
|
||||||
|
if command -v composer >/dev/null 2>&1; then
|
||||||
|
COMPOSER_BIN="$(command -v composer)"
|
||||||
|
check "Composer found at ${COMPOSER_BIN}"
|
||||||
|
elif [[ -f /usr/local/bin/composer ]]; then
|
||||||
|
COMPOSER_BIN="/usr/local/bin/composer"
|
||||||
|
check "Composer found at ${COMPOSER_BIN}"
|
||||||
|
else
|
||||||
|
warn "Composer not found — attempting auto-install..."
|
||||||
|
curl -fsSL https://getcomposer.org/installer | $PHP_BIN -- --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 \
|
||||||
|
&& COMPOSER_BIN="/usr/local/bin/composer" \
|
||||||
|
&& check "Composer installed" \
|
||||||
|
|| { fail "Composer not found — install from https://getcomposer.org"; DEPS_OK=false; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Git
|
||||||
|
if command -v git >/dev/null 2>&1; then
|
||||||
|
check "Git: $(git --version | head -1)"
|
||||||
|
else
|
||||||
|
fail "git not installed — installing..."
|
||||||
|
apt-get install -y git -q >/dev/null 2>&1 \
|
||||||
|
&& check "git installed" \
|
||||||
|
|| { fail "Could not install git"; DEPS_OK=false; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# curl
|
||||||
|
command -v curl >/dev/null 2>&1 && check "curl available" || { fail "curl not found"; DEPS_OK=false; }
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
NODE_VER="$(node --version 2>/dev/null)"
|
||||||
|
NODE_MAJ="${NODE_VER//v/}"; NODE_MAJ="${NODE_MAJ%%.*}"
|
||||||
|
if [[ "$NODE_MAJ" -ge 22 ]]; then
|
||||||
|
check "Node.js ${NODE_VER}"
|
||||||
|
else
|
||||||
|
warn "Node ${NODE_VER} is too old (panel requires >= 22). Will auto-upgrade."
|
||||||
|
# Upgrade via n
|
||||||
|
npm install -g n >/dev/null 2>&1 && n 22 >/dev/null 2>&1 && hash -r 2>/dev/null || true
|
||||||
|
NEW_VER="$(node --version 2>/dev/null)"
|
||||||
|
info "Node upgraded to ${NEW_VER}"
|
||||||
|
check "Node.js ${NEW_VER} (upgraded)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "Node.js not installed — installing via NodeSource..."
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - >/dev/null 2>&1
|
||||||
|
apt-get install -y nodejs >/dev/null 2>&1
|
||||||
|
check "Node.js $(node --version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
|
check "yarn: $(yarn --version)"
|
||||||
|
else
|
||||||
|
warn "yarn not found — installing..."
|
||||||
|
npm install -g yarn >/dev/null 2>&1 \
|
||||||
|
&& check "yarn installed" \
|
||||||
|
|| { fail "Could not install yarn"; DEPS_OK=false; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# python3 (needed for patching)
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
check "python3: $(python3 --version)"
|
||||||
|
else
|
||||||
|
apt-get install -y python3 -q >/dev/null 2>&1 \
|
||||||
|
&& check "python3 installed" \
|
||||||
|
|| { fail "python3 not available — needed for file patching"; DEPS_OK=false; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$DEPS_OK" == "true" ]] || error "Dependency checks failed. Fix the issues above and re-run."
|
||||||
|
success "All dependencies satisfied."
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# STEP 2: CONFIRM
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
PHP_BIN="$(command -v php)"
|
||||||
|
PANEL_VER="$(cd "$PANEL_DIR" && $PHP_BIN artisan --version 2>/dev/null | grep -oP '[\d\.]+' | head -1 || echo unknown)"
|
||||||
|
info "Panel version: ${PANEL_VER}"
|
||||||
|
|
||||||
|
[[ "$PANEL_VER" != "1.12"* ]] && [[ "$PANEL_VER" != "unknown" ]] && \
|
||||||
|
warn "Addon targets v1.12.x. Detected: v${PANEL_VER}. May still work."
|
||||||
|
|
||||||
# ── Confirm ───────────────────────────────────────────────────────────────────
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}The following actions will be taken:${NC}"
|
if [[ "$IS_UPDATE" == "true" ]]; then
|
||||||
echo " 1. Backup existing panel files to ${BACKUP_DIR}"
|
echo -e "${YELLOW}Update mode — will:${NC}"
|
||||||
echo " 2. Clone addon into ${TMP_DIR}"
|
echo " 1. Pull latest addon files from ${REPO_URL}"
|
||||||
echo " 3. Copy addon files into ${PANEL_DIR}"
|
echo " 2. Re-apply all patches (idempotent — skips already-applied ones)"
|
||||||
echo " 4. Apply minimal patches to 3 core files (documented in patches/PATCHES.md)"
|
echo " 3. Run any new migrations"
|
||||||
echo " 5. Run database migrations"
|
echo " 4. Ensure recharts is added to package.json"
|
||||||
echo " 6. Seed default roles"
|
echo " 5. Rebuild frontend"
|
||||||
echo " 7. Rebuild frontend assets (yarn build:production)"
|
echo " 6. Clear caches"
|
||||||
echo " 8. Clear panel caches"
|
else
|
||||||
|
echo -e "${YELLOW}Fresh install — will:${NC}"
|
||||||
|
echo " 1. Backup core panel files → ${BACKUP_DIR}"
|
||||||
|
echo " 2. Clone addon → ${TMP_DIR}"
|
||||||
|
echo " 3. Copy addon files into panel"
|
||||||
|
echo " 4. Apply patches (AppServiceProvider, routes.ts, FileEditContainer, Console)"
|
||||||
|
echo " 5. Run database migrations + seed default roles"
|
||||||
|
echo " 6. Add recharts dependency + rebuild frontend"
|
||||||
|
echo " 7. Clear caches"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
read -rp "Continue? [y/N] " CONFIRM
|
read -rp "Continue? [y/N] " CONFIRM
|
||||||
[[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; exit 0; }
|
[[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; exit 0; }
|
||||||
|
|
||||||
# ── Backup ────────────────────────────────────────────────────────────────────
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
step "Backing up panel to ${BACKUP_DIR}..."
|
# STEP 3: BACKUP
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
step "Backing up patched files..."
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
cp "${PANEL_DIR}/app/Providers/AppServiceProvider.php" "$BACKUP_DIR/" 2>/dev/null || true
|
for F in \
|
||||||
cp "${PANEL_DIR}/resources/scripts/routers/ServerRouter.tsx" "$BACKUP_DIR/" 2>/dev/null || true
|
"app/Providers/AppServiceProvider.php" \
|
||||||
cp "${PANEL_DIR}/resources/scripts/components/server/files/FileEditContainer.tsx" "$BACKUP_DIR/" 2>/dev/null || true
|
"resources/scripts/routers/routes.ts" \
|
||||||
cp "${PANEL_DIR}/resources/scripts/components/server/console/ServerConsole.tsx" "$BACKUP_DIR/" 2>/dev/null || true
|
"resources/scripts/routers/ServerRouter.tsx" \
|
||||||
success "Backup complete → ${BACKUP_DIR}"
|
"resources/scripts/components/server/files/FileEditContainer.tsx" \
|
||||||
|
"resources/scripts/components/server/console/Console.tsx"; do
|
||||||
|
[[ -f "${PANEL_DIR}/${F}" ]] && cp "${PANEL_DIR}/${F}" "$BACKUP_DIR/" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
success "Backup → ${BACKUP_DIR}"
|
||||||
|
|
||||||
# ── Clone Addon ───────────────────────────────────────────────────────────────
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
step "Cloning addon repository..."
|
# STEP 4: CLONE / PULL ADDON
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
step "Fetching addon from repository..."
|
||||||
git clone --depth=1 "$REPO_URL" "$TMP_DIR/addon" \
|
git clone --depth=1 "$REPO_URL" "$TMP_DIR/addon" \
|
||||||
|| error "Failed to clone ${REPO_URL}. Check network access and repository visibility."
|
|| error "Failed to clone ${REPO_URL}. Check network and repo visibility."
|
||||||
success "Clone complete."
|
success "Addon fetched."
|
||||||
|
|
||||||
# ── Copy Addon Files ──────────────────────────────────────────────────────────
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
step "Copying addon files into panel..."
|
# STEP 5: COPY FILES
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
step "Copying addon files..."
|
||||||
cd "$TMP_DIR/addon"
|
cd "$TMP_DIR/addon"
|
||||||
|
|
||||||
# Backend
|
cp -r app/Addons "${PANEL_DIR}/app/"
|
||||||
cp -r app/Addons "${PANEL_DIR}/app/"
|
cp config/advanced-admin.php "${PANEL_DIR}/config/"
|
||||||
# Config
|
cp -r config/addon-commands "${PANEL_DIR}/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"
|
mkdir -p "${PANEL_DIR}/database/migrations/addon"
|
||||||
cp -r database/migrations/addon/. "${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-client.php "${PANEL_DIR}/routes/"
|
cp routes/addon-application.php "${PANEL_DIR}/routes/"
|
||||||
cp routes/addon-application.php "${PANEL_DIR}/routes/"
|
|
||||||
# Frontend
|
|
||||||
mkdir -p "${PANEL_DIR}/resources/scripts/addons"
|
mkdir -p "${PANEL_DIR}/resources/scripts/addons"
|
||||||
cp -r resources/scripts/addons/advanced-admin "${PANEL_DIR}/resources/scripts/addons/"
|
cp -r resources/scripts/addons/advanced-admin \
|
||||||
# Seeders
|
"${PANEL_DIR}/resources/scripts/addons/"
|
||||||
cp -r database/seeders/. "${PANEL_DIR}/database/seeders/"
|
cp -r database/seeders/. "${PANEL_DIR}/database/seeders/"
|
||||||
# Tests
|
[[ -d tests/Addons ]] && cp -r tests/Addons "${PANEL_DIR}/tests/" || true
|
||||||
cp -r tests/Addons "${PANEL_DIR}/tests/"
|
|
||||||
|
|
||||||
success "Files copied."
|
success "Files copied."
|
||||||
|
|
||||||
# ── Apply Core Patches ────────────────────────────────────────────────────────
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
step "Applying core patches..."
|
# STEP 6: APPLY ALL PATCHES (idempotent Python script)
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
step "Applying patches..."
|
||||||
|
|
||||||
apply_patch() {
|
PATCH_SCRIPT="$(mktemp /tmp/addon_patch_XXXXXX.py)"
|
||||||
local FILE="$1"
|
cat > "$PATCH_SCRIPT" << 'PYEOF'
|
||||||
local SEARCH="$2"
|
import sys, re, os
|
||||||
local REPLACE="$3"
|
|
||||||
local LABEL="$4"
|
|
||||||
|
|
||||||
# Check if the injected content is already present (not the anchor)
|
PANEL = sys.argv[1]
|
||||||
if grep -qF "$REPLACE" "$FILE"; then
|
|
||||||
warn "Patch '${LABEL}' already applied — skipping."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -qF "$SEARCH" "$FILE"; then
|
def read(p): return open(p).read()
|
||||||
warn "Anchor '${SEARCH}' not found in ${FILE} — skipping patch '${LABEL}'."
|
def write(p, c): open(p, 'w').write(c)
|
||||||
return 0
|
def patch(path, label, check, anchor, inject):
|
||||||
fi
|
c = read(path)
|
||||||
|
if check in c:
|
||||||
|
print(f" [skip] {label} — already applied")
|
||||||
|
return
|
||||||
|
if anchor not in c:
|
||||||
|
print(f" [warn] {label} — anchor not found, skipping")
|
||||||
|
return
|
||||||
|
write(path, c.replace(anchor, anchor + '\n' + inject, 1))
|
||||||
|
print(f" [ ok ] {label}")
|
||||||
|
|
||||||
python3 -c "
|
# 1. AppServiceProvider
|
||||||
import sys
|
patch(
|
||||||
content = open('$FILE').read()
|
f'{PANEL}/app/Providers/AppServiceProvider.php',
|
||||||
if '''$REPLACE''' in content:
|
'Register AddonServiceProvider',
|
||||||
sys.exit(0)
|
'AddonServiceProvider::class',
|
||||||
if '''$SEARCH''' not in content:
|
' public function register(): void\n {',
|
||||||
sys.stderr.write('WARN: anchor not found in $FILE\n')
|
" $this->app->register(\\Pterodactyl\\Addons\\AdvancedAdmin\\AddonServiceProvider::class);"
|
||||||
sys.exit(0)
|
)
|
||||||
content = content.replace('''$SEARCH''', '''$SEARCH\n$REPLACE''', 1)
|
|
||||||
open('$FILE','w').write(content)
|
|
||||||
"
|
|
||||||
success "Patch applied: ${LABEL}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Patch 1: AppServiceProvider — register addon
|
# 2. routes.ts — imports
|
||||||
ASP="${PANEL_DIR}/app/Providers/AppServiceProvider.php"
|
rts = f'{PANEL}/resources/scripts/routers/routes.ts'
|
||||||
apply_patch "$ASP" \
|
c = read(rts)
|
||||||
"public function register(): void" \
|
lines = c.splitlines(keepends=True)
|
||||||
" \$this->app->register(\Pterodactyl\Addons\AdvancedAdmin\AddonServiceProvider::class);" \
|
last_import = max((i for i, l in enumerate(lines) if l.strip().startswith('import ')), default=0)
|
||||||
"Register AddonServiceProvider"
|
added = []
|
||||||
|
for check, imp in [
|
||||||
|
('NetworkTab', "import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';"),
|
||||||
|
('RolesPage', "import RolesPage from '@/addons/advanced-admin/roles/RolesPage';"),
|
||||||
|
]:
|
||||||
|
if check not in c:
|
||||||
|
lines.insert(last_import + 1, imp + '\n')
|
||||||
|
last_import += 1
|
||||||
|
added.append(check)
|
||||||
|
print(f" [ ok ] routes.ts import: {check}")
|
||||||
|
else:
|
||||||
|
print(f" [skip] routes.ts import: {check}")
|
||||||
|
c = ''.join(lines)
|
||||||
|
|
||||||
# Patch 2: ServerRouter — add Network tab route
|
# 2b. routes.ts — route entries
|
||||||
SR="${PANEL_DIR}/resources/scripts/routers/ServerRouter.tsx"
|
for rpath, block in [
|
||||||
if [[ -f "$SR" ]]; then
|
('/addon-network', """,
|
||||||
if ! grep -q "addon-network" "$SR"; then
|
{
|
||||||
python3 -c "
|
path: '/addon-network',
|
||||||
content = open('$SR').read()
|
permission: null,
|
||||||
import_line = \"import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';\"
|
name: 'Live Network',
|
||||||
route_line = \" <Route path=\\\"/server/:id/network\\\" element={<NetworkTab />} />\"
|
component: NetworkTab,
|
||||||
# Insert import after last existing addon import or at top of imports block
|
}"""),
|
||||||
if import_line not in content:
|
('/addon-roles', """,
|
||||||
content = content.replace(\"import React\", import_line + \"\nimport React\", 1)
|
{
|
||||||
if route_line not in content:
|
path: '/addon-roles',
|
||||||
content = content.replace(\"</Routes>\", route_line + \"\n </Routes>\", 1)
|
permission: null,
|
||||||
open('$SR','w').write(content)
|
name: 'Roles',
|
||||||
"
|
component: RolesPage,
|
||||||
success "Patch applied: ServerRouter NetworkTab"
|
}"""),
|
||||||
else
|
]:
|
||||||
warn "ServerRouter patch already applied — skipping."
|
if rpath in c:
|
||||||
fi
|
print(f" [skip] routes.ts route: {rpath}")
|
||||||
fi
|
else:
|
||||||
|
target = " ],\n} as Routes;"
|
||||||
|
if target in c:
|
||||||
|
c = c.replace(target, f"{block},\n ],\n}} as Routes;")
|
||||||
|
print(f" [ ok ] routes.ts route: {rpath}")
|
||||||
|
else:
|
||||||
|
print(f" [warn] routes.ts: closing not found for {rpath}")
|
||||||
|
write(rts, c)
|
||||||
|
|
||||||
# Patch 3: FileEditContainer — inject history button
|
# 3. FileEditContainer — import
|
||||||
FEC="${PANEL_DIR}/resources/scripts/components/server/files/FileEditContainer.tsx"
|
fec = f'{PANEL}/resources/scripts/components/server/files/FileEditContainer.tsx'
|
||||||
if [[ -f "$FEC" ]]; then
|
patch(fec, 'FileHistoryButton import', 'FileHistoryButton',
|
||||||
if ! grep -q "FileHistoryButton" "$FEC"; then
|
"import { encodePathSegments, hashToPath } from '@/helpers';",
|
||||||
python3 -c "
|
"import FileHistoryButton from '@/addons/advanced-admin/revisions/FileHistoryButton';"
|
||||||
content = open('$FEC').read()
|
)
|
||||||
import_line = \"import FileHistoryButton from '@/addons/advanced-admin/revisions/FileHistoryButton';\"
|
# 3b. FileEditContainer — JSX button
|
||||||
if import_line not in content:
|
patch(fec, 'FileHistoryButton JSX', '<FileHistoryButton',
|
||||||
content = content.replace(\"import React\", import_line + \"\nimport React\", 1)
|
" <div css={tw`flex justify-end mt-4`}>",
|
||||||
open('$FEC','w').write(content)
|
" {action === 'edit' && <FileHistoryButton filePath={hash.replace(/^#/, '')} />}"
|
||||||
"
|
)
|
||||||
success "Patch applied: FileEditContainer FileHistoryButton"
|
|
||||||
else
|
|
||||||
warn "FileEditContainer patch already applied — skipping."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Patch 4: ServerConsole — wrap command input
|
# 4. Console.tsx — CommandInput
|
||||||
SC="${PANEL_DIR}/resources/scripts/components/server/console/ServerConsole.tsx"
|
con = f'{PANEL}/resources/scripts/components/server/console/Console.tsx'
|
||||||
if [[ -f "$SC" ]]; then
|
patch(con, 'CommandInput import', 'CommandInput',
|
||||||
if ! grep -q "CommandInput" "$SC"; then
|
"import { ServerContext } from '@/state/server';",
|
||||||
python3 -c "
|
"import CommandInput from '@/addons/advanced-admin/console/CommandInput';"
|
||||||
content = open('$SC').read()
|
)
|
||||||
import_line = \"import CommandInput from '@/addons/advanced-admin/console/CommandInput';\"
|
# 4b. Inject CommandInput (replace native input)
|
||||||
if import_line not in content:
|
c = read(con)
|
||||||
content = content.replace(\"import React\", import_line + \"\nimport React\", 1)
|
if '<CommandInput' not in c:
|
||||||
open('$SC','w').write(content)
|
old = " <input\n className={classNames('peer', styles.command_input)}"
|
||||||
"
|
new = """ <CommandInput
|
||||||
success "Patch applied: ServerConsole CommandInput"
|
onSend={(cmd) => {
|
||||||
else
|
setHistory((prev: string[]) => [cmd, ...(prev || [])].slice(0, 32));
|
||||||
warn "ServerConsole patch already applied — skipping."
|
instance && instance.send('send command', cmd);
|
||||||
fi
|
}}
|
||||||
fi
|
disabled={!connected}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className={classNames('peer', styles.command_input)}
|
||||||
|
style={{ display: 'none' }}"""
|
||||||
|
if old in c:
|
||||||
|
write(con, c.replace(old, new, 1))
|
||||||
|
print(" [ ok ] Console.tsx CommandInput injected")
|
||||||
|
else:
|
||||||
|
print(" [warn] Console.tsx: native input anchor not found")
|
||||||
|
else:
|
||||||
|
print(" [skip] Console.tsx CommandInput — already injected")
|
||||||
|
|
||||||
success "All patches applied."
|
# 5. Clean any stray imports from ServerRouter.tsx
|
||||||
|
sr = f'{PANEL}/resources/scripts/routers/ServerRouter.tsx'
|
||||||
|
c = read(sr)
|
||||||
|
changed = False
|
||||||
|
for stray in [
|
||||||
|
"import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';\n",
|
||||||
|
"import RolesPage from '@/addons/advanced-admin/roles/RolesPage';\n",
|
||||||
|
]:
|
||||||
|
if stray in c:
|
||||||
|
c = c.replace(stray, '')
|
||||||
|
changed = True
|
||||||
|
print(f" [ ok ] Removed stray import from ServerRouter")
|
||||||
|
if changed:
|
||||||
|
write(sr, c)
|
||||||
|
else:
|
||||||
|
print(" [skip] ServerRouter.tsx — no stray imports")
|
||||||
|
|
||||||
# ── Database Migrations ───────────────────────────────────────────────────────
|
# 6. Fix route middleware (remove unregistered throttle limiters)
|
||||||
|
for rf in ['addon-client.php', 'addon-application.php']:
|
||||||
|
path = f'{PANEL}/routes/{rf}'
|
||||||
|
if not os.path.exists(path): continue
|
||||||
|
c = read(path)
|
||||||
|
cleaned = re.sub(r"->middleware\('throttle:addon-[^']+'\)", "", c)
|
||||||
|
cleaned = re.sub(r",?\s*'addon\.ratelimit'", "", cleaned)
|
||||||
|
if cleaned != c:
|
||||||
|
write(path, cleaned)
|
||||||
|
print(f" [ ok ] Cleaned middleware: {rf}")
|
||||||
|
else:
|
||||||
|
print(f" [skip] Middleware clean: {rf}")
|
||||||
|
|
||||||
|
print("\nAll patches done.")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
python3 "$PATCH_SCRIPT" "$PANEL_DIR"
|
||||||
|
rm -f "$PATCH_SCRIPT"
|
||||||
|
success "Patches applied."
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# STEP 7: DATABASE MIGRATIONS
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
step "Running database migrations..."
|
step "Running database migrations..."
|
||||||
cd "$PANEL_DIR"
|
cd "$PANEL_DIR"
|
||||||
$PHP_BIN artisan migrate --path=database/migrations/addon --force \
|
$PHP_BIN artisan migrate --path=database/migrations/addon --force \
|
||||||
|| error "Migration failed. Check your database connection and logs."
|
|| warn "Migration issues — check logs. May be already run."
|
||||||
success "Migrations complete."
|
success "Migrations complete."
|
||||||
|
|
||||||
# ── Seed Default Roles ────────────────────────────────────────────────────────
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# STEP 8: SEED DEFAULT ROLES
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
step "Seeding default roles..."
|
step "Seeding default roles..."
|
||||||
cd "$PANEL_DIR"
|
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)"
|
SEED_SCRIPT="$(mktemp /tmp/addon_seed_XXXXXX.php)"
|
||||||
cat > "$SEED_SCRIPT" <<'PHPEOF'
|
PANEL_ESC="${PANEL_DIR//\'/\'}"
|
||||||
|
cat > "$SEED_SCRIPT" << PHPEOF
|
||||||
<?php
|
<?php
|
||||||
define('LARAVEL_START', microtime(true));
|
define('LARAVEL_START', microtime(true));
|
||||||
require __DIR__ . '/vendor/autoload.php';
|
require '${PANEL_ESC}/vendor/autoload.php';
|
||||||
$app = require_once __DIR__ . '/bootstrap/app.php';
|
\$app = require_once '${PANEL_ESC}/bootstrap/app.php';
|
||||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
\$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||||
$kernel->bootstrap();
|
\$kernel->bootstrap();
|
||||||
require_once __DIR__ . '/database/seeders/DefaultRolesSeeder.php';
|
require_once '${PANEL_ESC}/database/seeders/DefaultRolesSeeder.php';
|
||||||
$seeder = new Database\Seeders\DefaultRolesSeeder();
|
\$seeder = new Database\Seeders\DefaultRolesSeeder();
|
||||||
$seeder->setContainer($app);
|
\$seeder->setContainer(\$app);
|
||||||
$seeder->run();
|
\$seeder->run();
|
||||||
echo "Roles seeded successfully.\n";
|
echo "Roles seeded.\n";
|
||||||
PHPEOF
|
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."
|
$PHP_BIN "$SEED_SCRIPT" 2>&1 || warn "Seeder failed or already run — skipping."
|
||||||
rm -f "$SEED_SCRIPT"
|
rm -f "$SEED_SCRIPT"
|
||||||
success "Seeding complete."
|
success "Seeding complete."
|
||||||
|
|
||||||
# ── Publish Config ────────────────────────────────────────────────────────────
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
step "Publishing config..."
|
# STEP 9: ADD ENV VARS (if not present)
|
||||||
$PHP_BIN artisan vendor:publish --tag=advanced-admin-config --force 2>/dev/null || true
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
success "Config published."
|
step "Checking .env addon vars..."
|
||||||
|
ENV_FILE="${PANEL_DIR}/.env"
|
||||||
|
if [[ -f "$ENV_FILE" ]] && ! grep -q "ADDON_NETWORK_ENABLED" "$ENV_FILE"; then
|
||||||
|
cat >> "$ENV_FILE" << 'ENVEOF'
|
||||||
|
|
||||||
# ── Build Frontend ────────────────────────────────────────────────────────────
|
# ── Pterodactyl Advanced Admin Addons ─────────────────────────────────────────
|
||||||
step "Building frontend assets (this may take a few minutes)..."
|
ADDON_NETWORK_ENABLED=true
|
||||||
command -v yarn >/dev/null 2>&1 || error "yarn is not installed. Install with: npm install -g yarn"
|
ADDON_NETWORK_INTERVAL=10
|
||||||
|
ADDON_NETWORK_IPTABLES=false
|
||||||
cd "$PANEL_DIR"
|
ADDON_NETWORK_RETENTION_RAW=1
|
||||||
|
ADDON_NETWORK_RETENTION_5M=7
|
||||||
# Ensure Node >=22 (panel requires it; upgrade via 'n' if needed)
|
ADDON_NETWORK_RETENTION_1H=30
|
||||||
NODE_MAJ="$(node --version 2>/dev/null | sed 's/v//;s/\..*//' || echo 0)"
|
ADDON_NETWORK_RETENTION_1D=365
|
||||||
if [[ "${NODE_MAJ}" -lt 22 ]]; then
|
ADDON_ROLES_ENABLED=true
|
||||||
info "Node ${NODE_MAJ} is too old (need >=22). Upgrading via 'n'..."
|
ADDON_REVISIONS_ENABLED=true
|
||||||
npm install -g n >/dev/null 2>&1 \
|
ADDON_REVISIONS_MAX_FILE_SIZE=10485760
|
||||||
&& n 22 >/dev/null 2>&1 \
|
ADDON_REVISIONS_MAX_PER_FILE=50
|
||||||
&& hash -r 2>/dev/null || true
|
ADDON_REVISIONS_RETENTION_DAYS=90
|
||||||
info "Node version now: $(node --version 2>/dev/null || echo unknown)"
|
ADDON_CONSOLE_ENABLED=true
|
||||||
|
ADDON_CONSOLE_RATE_LIMIT=60
|
||||||
|
ADDON_CONSOLE_PLAYER_TTL=30
|
||||||
|
ENVEOF
|
||||||
|
success ".env vars added."
|
||||||
|
else
|
||||||
|
info ".env addon vars already present — skipping."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export NODE_OPTIONS="${NODE_OPTIONS:---openssl-legacy-provider}"
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
yarn install --frozen-lockfile 2>&1 | tail -5
|
# STEP 10: BUILD FRONTEND
|
||||||
yarn build:production 2>&1 | tail -30
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
step "Building frontend assets..."
|
||||||
|
cd "$PANEL_DIR"
|
||||||
|
|
||||||
|
# Ensure recharts is in package.json (survives yarn install --frozen-lockfile)
|
||||||
|
if ! grep -q '"recharts"' package.json 2>/dev/null; then
|
||||||
|
info "Adding recharts@2.12.7 to package.json..."
|
||||||
|
yarn add recharts@2.12.7 --no-lockfile >/dev/null 2>&1
|
||||||
|
success "recharts added."
|
||||||
|
else
|
||||||
|
info "recharts already in package.json."
|
||||||
|
fi
|
||||||
|
|
||||||
|
yarn install 2>&1 | tail -3
|
||||||
|
yarn build:production 2>&1 | tail -15
|
||||||
success "Frontend built."
|
success "Frontend built."
|
||||||
|
|
||||||
# ── Clear Caches ──────────────────────────────────────────────────────────────
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
step "Clearing caches..."
|
# STEP 11: CACHES + PERMISSIONS
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
step "Clearing caches and fixing permissions..."
|
||||||
|
cd "$PANEL_DIR"
|
||||||
$PHP_BIN artisan optimize:clear
|
$PHP_BIN artisan optimize:clear
|
||||||
$PHP_BIN artisan optimize
|
$PHP_BIN artisan optimize
|
||||||
success "Caches cleared."
|
$PHP_BIN artisan queue:restart
|
||||||
|
|
||||||
# ── Fix Permissions ───────────────────────────────────────────────────────────
|
chown -R www-data:www-data \
|
||||||
step "Fixing file permissions..."
|
"${PANEL_DIR}/storage" \
|
||||||
chown -R www-data:www-data "${PANEL_DIR}/storage" "${PANEL_DIR}/bootstrap/cache" \
|
"${PANEL_DIR}/bootstrap/cache" \
|
||||||
"${PANEL_DIR}/app/Addons" "${PANEL_DIR}/resources/scripts/addons" 2>/dev/null || true
|
"${PANEL_DIR}/app/Addons" \
|
||||||
success "Permissions set."
|
"${PANEL_DIR}/resources/scripts/addons" 2>/dev/null || true
|
||||||
|
|
||||||
|
success "Caches cleared and permissions set."
|
||||||
|
|
||||||
|
# ── Mark installation ─────────────────────────────────────────────────────────
|
||||||
|
echo "$ADDON_VERSION" > "$ADDON_MARKER"
|
||||||
|
|
||||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════╗
|
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════════╗
|
||||||
║ ✅ Installation complete! ║
|
║ ✅ $([ "$IS_UPDATE" == "true" ] && echo "Update" || echo "Installation") complete! v${ADDON_VERSION} ║
|
||||||
╚══════════════════════════════════════════════════════╝${NC}"
|
╚══════════════════════════════════════════════════════════╝${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " Panel path : ${CYAN}${PANEL_DIR}${NC}"
|
echo -e " Panel path : ${CYAN}${PANEL_DIR}${NC}"
|
||||||
echo -e " Backup path : ${CYAN}${BACKUP_DIR}${NC}"
|
echo -e " Backup path : ${CYAN}${BACKUP_DIR}${NC}"
|
||||||
echo -e " Addon repo : ${CYAN}${REPO_URL}${NC}"
|
echo -e " Addon repo : ${CYAN}${REPO_URL}${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${YELLOW}Next steps:${NC}"
|
echo -e " ${BOLD}Active modules:${NC}"
|
||||||
echo " 1. Review config: ${PANEL_DIR}/config/advanced-admin.php"
|
echo " 🌐 Live Network — server nav tab → /addon-network"
|
||||||
echo " 2. Add ADDON_* env vars to .env if needed (see docs/CONFIG.md)"
|
echo " 🛡 Roles — server nav tab → /addon-roles"
|
||||||
echo " 3. Restart your queue worker: php artisan queue:restart"
|
echo " 🕐 File History — button in file editor toolbar"
|
||||||
echo " 4. Visit your panel — four new modules are now active!"
|
echo " 🤖 Autocomplete — enhanced console command input"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}To update in the future, run:${NC}"
|
||||||
|
echo " bash <(curl -fsSL ${RAW_URL}/install.sh) --update"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
102
resources/scripts/addons/advanced-admin/roles/RolesPage.tsx
Normal file
102
resources/scripts/addons/advanced-admin/roles/RolesPage.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Roles Management Page — Admin view for role-based permissions
|
||||||
|
* Ball Studios <https://git.balls.studio>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import http from '@/api/http';
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
|
interface Role {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
description: string;
|
||||||
|
permissions: string[];
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserRole {
|
||||||
|
role: Role;
|
||||||
|
override: string | null;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RolesPage() {
|
||||||
|
const uuid = ServerContext.useStoreState((s) => s.server.data!.uuid);
|
||||||
|
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
http.get(`/api/client/servers/${uuid}/addons/permissions/effective/me`)
|
||||||
|
.then(({ data }: any) => setUserRoles(data.data ?? []))
|
||||||
|
.catch(() => setError('Failed to load role data.'))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [uuid]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContentBlock title={'Role Permissions'}>
|
||||||
|
<div css={tw`mb-6`}>
|
||||||
|
<h2 css={tw`text-2xl font-bold text-neutral-100 mb-1`}>🛡 Role Permissions</h2>
|
||||||
|
<p css={tw`text-neutral-400 text-sm`}>
|
||||||
|
Your effective permissions for this server, managed by the Ball Studios Advanced Admin addon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div css={tw`text-neutral-400 py-8 text-center`}>Loading permissions…</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div css={tw`p-4 rounded bg-red-900 border border-red-600 text-red-200 text-sm mb-4`}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && userRoles.length === 0 && (
|
||||||
|
<div css={tw`p-6 rounded bg-neutral-800 border border-neutral-700 text-neutral-400 text-sm text-center`}>
|
||||||
|
No roles assigned. Contact an administrator.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div css={tw`grid gap-4`}>
|
||||||
|
{userRoles.map((ur, i) => (
|
||||||
|
<div key={i} css={tw`p-4 rounded-lg bg-neutral-800 border border-neutral-700`}>
|
||||||
|
<div css={tw`flex items-center gap-3 mb-2`}>
|
||||||
|
<span
|
||||||
|
css={tw`w-3 h-3 rounded-full inline-block flex-shrink-0`}
|
||||||
|
style={{ backgroundColor: ur.role?.color ?? '#6366f1' }}
|
||||||
|
/>
|
||||||
|
<span css={tw`font-semibold text-neutral-100`}>
|
||||||
|
{ur.role?.name ?? 'Unknown Role'}
|
||||||
|
</span>
|
||||||
|
{ur.source && (
|
||||||
|
<span css={tw`ml-auto text-xs text-neutral-500 bg-neutral-900 px-2 py-0.5 rounded`}>
|
||||||
|
via {ur.source}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{ur.role?.description && (
|
||||||
|
<p css={tw`text-neutral-400 text-sm mb-3`}>{ur.role.description}</p>
|
||||||
|
)}
|
||||||
|
{ur.role?.permissions?.length > 0 && (
|
||||||
|
<div css={tw`flex flex-wrap gap-1`}>
|
||||||
|
{ur.role.permissions.map((perm: string) => (
|
||||||
|
<span
|
||||||
|
key={perm}
|
||||||
|
css={tw`text-xs font-mono px-2 py-0.5 rounded bg-indigo-900 text-indigo-300 border border-indigo-700`}
|
||||||
|
>
|
||||||
|
{perm}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PageContentBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,7 +16,12 @@ use Pterodactyl\Addons\AdvancedAdmin\Http\Controllers\Client\AutocompleteControl
|
|||||||
|
|
||||||
Route::group([
|
Route::group([
|
||||||
'prefix' => 'api/client/servers/{server}',
|
'prefix' => 'api/client/servers/{server}',
|
||||||
'middleware' => ['api', 'auth:sanctum', 'addon.ratelimit'],
|
'middleware' => [
|
||||||
|
'api',
|
||||||
|
'auth:sanctum',
|
||||||
|
'addon.ratelimit',
|
||||||
|
\Pterodactyl\Http\Middleware\Api\Client\SubstituteClientBindings::class,
|
||||||
|
],
|
||||||
], function () {
|
], function () {
|
||||||
|
|
||||||
// ── Network Traffic ───────────────────────────────────────────────────────
|
// ── Network Traffic ───────────────────────────────────────────────────────
|
||||||
|
|||||||
197
scripts/patch_all_modules.py
Normal file
197
scripts/patch_all_modules.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Patch all 4 addon modules into the panel frontend:
|
||||||
|
1. Live Network → /addon-network nav tab (NetworkTab.tsx)
|
||||||
|
2. File History → FileHistoryButton injected into FileEditContainer.tsx
|
||||||
|
3. Console Autocomplete → CommandInput replaces bare <input> in Console.tsx
|
||||||
|
4. Roles → /addon-roles nav tab (RolesPage.tsx)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys, os, shutil
|
||||||
|
|
||||||
|
PANEL = '/var/www/pterodactyl'
|
||||||
|
ADDON_SRC = '/var/www/pterodactyl/resources/scripts/addons/advanced-admin'
|
||||||
|
|
||||||
|
def read(path): return open(path).read()
|
||||||
|
def write(path, content): open(path, 'w').write(content)
|
||||||
|
|
||||||
|
def patch_file(path, label, check_str, old, new):
|
||||||
|
content = read(path)
|
||||||
|
if check_str in content:
|
||||||
|
print(f" [{label}] Already patched")
|
||||||
|
return False
|
||||||
|
if old not in content:
|
||||||
|
print(f" [{label}] WARN: anchor not found — check manually")
|
||||||
|
return False
|
||||||
|
write(path, content.replace(old, new, 1))
|
||||||
|
print(f" [{label}] ✅ Patched")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 1. routes.ts — register all 4 tabs
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n=== 1. routes.ts ===")
|
||||||
|
rts = f'{PANEL}/resources/scripts/routers/routes.ts'
|
||||||
|
content = read(rts)
|
||||||
|
|
||||||
|
# Imports to inject
|
||||||
|
IMPORTS = [
|
||||||
|
('NetworkTab', "import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';"),
|
||||||
|
('RolesPage', "import RolesPage from '@/addons/advanced-admin/roles/RolesPage';"),
|
||||||
|
]
|
||||||
|
|
||||||
|
lines = content.splitlines(keepends=True)
|
||||||
|
last_import_idx = max(i for i, l in enumerate(lines) if l.strip().startswith('import '))
|
||||||
|
|
||||||
|
inserts = []
|
||||||
|
for check, imp in IMPORTS:
|
||||||
|
if check not in content:
|
||||||
|
inserts.append(imp + '\n')
|
||||||
|
print(f" Adding import: {check}")
|
||||||
|
else:
|
||||||
|
print(f" Import exists: {check}")
|
||||||
|
|
||||||
|
if inserts:
|
||||||
|
for imp in reversed(inserts):
|
||||||
|
lines.insert(last_import_idx + 1, imp)
|
||||||
|
content = ''.join(lines)
|
||||||
|
|
||||||
|
# Routes to append before closing of server array
|
||||||
|
ROUTES = [
|
||||||
|
('/addon-network', """,
|
||||||
|
{
|
||||||
|
path: '/addon-network',
|
||||||
|
permission: null,
|
||||||
|
name: 'Live Network',
|
||||||
|
component: NetworkTab,
|
||||||
|
}"""),
|
||||||
|
('/addon-roles', """,
|
||||||
|
{
|
||||||
|
path: '/addon-roles',
|
||||||
|
permission: null,
|
||||||
|
name: 'Roles',
|
||||||
|
component: RolesPage,
|
||||||
|
}"""),
|
||||||
|
]
|
||||||
|
|
||||||
|
for route_path, block in ROUTES:
|
||||||
|
if route_path not in content:
|
||||||
|
target = " ],\n} as Routes;"
|
||||||
|
if target in content:
|
||||||
|
content = content.replace(target, f"{block},\n ],\n}} as Routes;")
|
||||||
|
print(f" Added route: {route_path}")
|
||||||
|
else:
|
||||||
|
print(f" WARN: insertion point not found for {route_path}")
|
||||||
|
else:
|
||||||
|
print(f" Route exists: {route_path}")
|
||||||
|
|
||||||
|
write(rts, content)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 2. FileEditContainer.tsx — inject FileHistoryButton
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n=== 2. FileEditContainer.tsx — History button ===")
|
||||||
|
fec = f'{PANEL}/resources/scripts/components/server/files/FileEditContainer.tsx'
|
||||||
|
|
||||||
|
# Step A: add import
|
||||||
|
patch_file(
|
||||||
|
fec, 'import',
|
||||||
|
'FileHistoryButton',
|
||||||
|
"import { encodePathSegments, hashToPath } from '@/helpers';",
|
||||||
|
"import { encodePathSegments, hashToPath } from '@/helpers';\nimport FileHistoryButton from '@/addons/advanced-admin/revisions/FileHistoryButton';"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step B: inject button in the footer action row, before Save/Create buttons
|
||||||
|
patch_file(
|
||||||
|
fec, 'JSX button',
|
||||||
|
'<FileHistoryButton',
|
||||||
|
" <div css={tw`flex justify-end mt-4`}>",
|
||||||
|
" <div css={tw`flex justify-end mt-4 gap-2`}>\n {action === 'edit' && <FileHistoryButton filePath={hash.replace(/^#/, '')} />}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 3. Console.tsx — inject CommandInput wrapper
|
||||||
|
#
|
||||||
|
# The console's existing <input> handles history/keyboard; our CommandInput
|
||||||
|
# is a self-contained replacement that renders its own <input> + dropdown.
|
||||||
|
# We inject it BELOW the existing console output area, not replacing the input,
|
||||||
|
# so we don't break the existing keyboard shortcuts. Instead we ADD an
|
||||||
|
# autocomplete overlay above it.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n=== 3. Console.tsx — autocomplete ===")
|
||||||
|
con = f'{PANEL}/resources/scripts/components/server/console/Console.tsx'
|
||||||
|
|
||||||
|
# Add import
|
||||||
|
patch_file(
|
||||||
|
con, 'import',
|
||||||
|
'CommandInput',
|
||||||
|
"import { ServerContext } from '@/state/server';",
|
||||||
|
"import { ServerContext } from '@/state/server';\nimport CommandInput from '@/addons/advanced-admin/console/CommandInput';"
|
||||||
|
)
|
||||||
|
|
||||||
|
# We inject CommandInput ABOVE the existing input row as an enhancement panel.
|
||||||
|
# It sits above the native input and provides suggestions; the native input
|
||||||
|
# still works for history navigation (up/down arrow).
|
||||||
|
content = read(con)
|
||||||
|
if 'CommandInput' in content and '<CommandInput' not in content:
|
||||||
|
# Find the container div that wraps the input
|
||||||
|
old = " <input\n className={classNames('peer', styles.command_input)}"
|
||||||
|
new = """ <CommandInput
|
||||||
|
onSend={(cmd) => {
|
||||||
|
setHistory((prev: string[]) => [cmd, ...(prev || [])].slice(0, 32));
|
||||||
|
instance && instance.send('send command', cmd);
|
||||||
|
}}
|
||||||
|
disabled={!connected}
|
||||||
|
/>
|
||||||
|
{/* native input kept for history arrow-key navigation */}
|
||||||
|
<input
|
||||||
|
className={classNames('peer', styles.command_input)}
|
||||||
|
style={{ display: 'none' }}"""
|
||||||
|
if old in content:
|
||||||
|
write(con, content.replace(old, new, 1))
|
||||||
|
print(" ✅ CommandInput injected (native input hidden, replaced)")
|
||||||
|
else:
|
||||||
|
print(" WARN: native input anchor not found — injecting before the input block")
|
||||||
|
# Fallback: add CommandInput just before the div that has command_input
|
||||||
|
fallback_old = " <input"
|
||||||
|
if fallback_old in content:
|
||||||
|
write(con, content.replace(
|
||||||
|
fallback_old,
|
||||||
|
""" <CommandInput
|
||||||
|
onSend={(cmd) => { instance && instance.send('send command', cmd); }}
|
||||||
|
disabled={!connected}
|
||||||
|
/>
|
||||||
|
<input style={{display:'none'}}""",
|
||||||
|
1
|
||||||
|
))
|
||||||
|
print(" ✅ CommandInput injected (fallback mode)")
|
||||||
|
elif '<CommandInput' in content:
|
||||||
|
print(" Already injected")
|
||||||
|
else:
|
||||||
|
print(" WARN: import added but no injection done — check Console.tsx manually")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 4. Clean ServerRouter.tsx stray imports
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
print("\n=== 4. ServerRouter.tsx cleanup ===")
|
||||||
|
sr = f'{PANEL}/resources/scripts/routers/ServerRouter.tsx'
|
||||||
|
content = read(sr)
|
||||||
|
changed = False
|
||||||
|
for stray in [
|
||||||
|
"import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';\n",
|
||||||
|
"import RolesPage from '@/addons/advanced-admin/roles/RolesPage';\n",
|
||||||
|
]:
|
||||||
|
if stray in content:
|
||||||
|
content = content.replace(stray, '')
|
||||||
|
print(f" Removed: {stray.strip()}")
|
||||||
|
changed = True
|
||||||
|
if not changed:
|
||||||
|
print(" Already clean")
|
||||||
|
write(sr, content)
|
||||||
|
|
||||||
|
|
||||||
|
print("\n✅ All patches applied. Run: cd /var/www/pterodactyl && yarn build:production")
|
||||||
87
scripts/patch_routes.py
Normal file
87
scripts/patch_routes.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Patch routes.ts and ServerRouter.tsx to inject addon routes (network tab nav link).
|
||||||
|
This patcher understands Pterodactyl's actual routing structure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
PANEL = '/var/www/pterodactyl'
|
||||||
|
ROUTES_FILE = f'{PANEL}/resources/scripts/routers/routes.ts'
|
||||||
|
SERVER_ROUTER = f'{PANEL}/resources/scripts/routers/ServerRouter.tsx'
|
||||||
|
|
||||||
|
# ─── 1. Patch routes.ts ───────────────────────────────────────────────────────
|
||||||
|
print("=== Patching routes.ts ===")
|
||||||
|
content = open(ROUTES_FILE).read()
|
||||||
|
|
||||||
|
ADDON_IMPORT = "import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';"
|
||||||
|
ADDON_ROUTE = """,
|
||||||
|
{
|
||||||
|
path: '/addon-network',
|
||||||
|
permission: null,
|
||||||
|
name: 'Live Network',
|
||||||
|
component: NetworkTab,
|
||||||
|
}"""
|
||||||
|
|
||||||
|
patched = False
|
||||||
|
|
||||||
|
# Add import near top if not present
|
||||||
|
if ADDON_IMPORT not in content:
|
||||||
|
# Insert after last existing import line
|
||||||
|
lines = content.splitlines(keepends=True)
|
||||||
|
last_import = 0
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip().startswith('import ') or line.strip().startswith('const ') and 'lazy' in line:
|
||||||
|
last_import = i
|
||||||
|
lines.insert(last_import + 1, ADDON_IMPORT + '\n')
|
||||||
|
content = ''.join(lines)
|
||||||
|
print(f" Added import at line {last_import + 2}")
|
||||||
|
else:
|
||||||
|
print(" Import already present")
|
||||||
|
|
||||||
|
# Add route entry before closing of server array
|
||||||
|
if '/addon-network' not in content:
|
||||||
|
# Find the last route entry (before closing ], server array end)
|
||||||
|
# Insert before the closing ` ],` that ends the server array
|
||||||
|
# The server array ends with the activity route, then ` ],`
|
||||||
|
insert_anchor = " {\n path: '/activity',\n permission: 'activity.*',\n name: 'Activity',\n component: ServerActivityLogContainer,"
|
||||||
|
closing = " },\n ],\n} as Routes;"
|
||||||
|
if insert_anchor in content and closing in content:
|
||||||
|
# Find and insert our route before the closing `] as Routes`
|
||||||
|
content = content.replace(
|
||||||
|
" },\n ],\n} as Routes;",
|
||||||
|
f" }},{ADDON_ROUTE},\n ],\n}} as Routes;"
|
||||||
|
)
|
||||||
|
patched = True
|
||||||
|
print(" Added /addon-network route entry")
|
||||||
|
else:
|
||||||
|
print(" WARNING: Could not find anchor for route insertion")
|
||||||
|
print(" Trying simpler approach...")
|
||||||
|
# Simpler: insert before `] as Routes`
|
||||||
|
if " ],\n} as Routes;" in content:
|
||||||
|
content = content.replace(
|
||||||
|
" ],\n} as Routes;",
|
||||||
|
f"{ADDON_ROUTE},\n ],\n}} as Routes;"
|
||||||
|
)
|
||||||
|
patched = True
|
||||||
|
print(" Added route entry (simple method)")
|
||||||
|
else:
|
||||||
|
print(" Route already present")
|
||||||
|
patched = True
|
||||||
|
|
||||||
|
open(ROUTES_FILE, 'w').write(content)
|
||||||
|
print(f" routes.ts saved. Patched={patched}")
|
||||||
|
|
||||||
|
# ─── 2. Clean up ServerRouter.tsx (remove the broken import we added) ─────────
|
||||||
|
print("\n=== Cleaning ServerRouter.tsx ===")
|
||||||
|
sr_content = open(SERVER_ROUTER).read()
|
||||||
|
bad_import = "import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';\n"
|
||||||
|
if bad_import in sr_content:
|
||||||
|
sr_content = sr_content.replace(bad_import, '')
|
||||||
|
open(SERVER_ROUTER, 'w').write(sr_content)
|
||||||
|
print(" Removed stray NetworkTab import from ServerRouter.tsx")
|
||||||
|
else:
|
||||||
|
print(" ServerRouter.tsx is clean")
|
||||||
|
|
||||||
|
print("\nDone. Now rebuild the frontend.")
|
||||||
97
scripts/repatch.py
Normal file
97
scripts/repatch.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Re-apply all addon patches after a panel update."""
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
PANEL = '/var/www/pterodactyl'
|
||||||
|
|
||||||
|
# ── 1. AppServiceProvider ─────────────────────────────────────────────────────
|
||||||
|
print("=== Patching AppServiceProvider.php ===")
|
||||||
|
asp = f'{PANEL}/app/Providers/AppServiceProvider.php'
|
||||||
|
content = open(asp).read()
|
||||||
|
INJECT = " $this->app->register(\\Pterodactyl\\Addons\\AdvancedAdmin\\AddonServiceProvider::class);"
|
||||||
|
if 'AddonServiceProvider' not in content:
|
||||||
|
old = ' public function register(): void\n {'
|
||||||
|
new = f' public function register(): void\n {{\n{INJECT}'
|
||||||
|
if old in content:
|
||||||
|
content = content.replace(old, new, 1)
|
||||||
|
open(asp, 'w').write(content)
|
||||||
|
print(f" Applied: AddonServiceProvider registered")
|
||||||
|
else:
|
||||||
|
print(f" WARN: anchor not found in AppServiceProvider")
|
||||||
|
else:
|
||||||
|
print(" Already patched")
|
||||||
|
|
||||||
|
# ── 2. routes.ts ─────────────────────────────────────────────────────────────
|
||||||
|
print("\n=== Patching routes.ts ===")
|
||||||
|
rts = f'{PANEL}/resources/scripts/routers/routes.ts'
|
||||||
|
content = open(rts).read()
|
||||||
|
|
||||||
|
IMPORT = "import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';"
|
||||||
|
ROUTE_BLOCK = """,
|
||||||
|
{
|
||||||
|
path: '/addon-network',
|
||||||
|
permission: null,
|
||||||
|
name: 'Live Network',
|
||||||
|
component: NetworkTab,
|
||||||
|
}"""
|
||||||
|
|
||||||
|
# Add import
|
||||||
|
if IMPORT not in content:
|
||||||
|
lines = content.splitlines(keepends=True)
|
||||||
|
last_import = 0
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip().startswith('import '):
|
||||||
|
last_import = i
|
||||||
|
lines.insert(last_import + 1, IMPORT + '\n')
|
||||||
|
content = ''.join(lines)
|
||||||
|
print(" Added import")
|
||||||
|
else:
|
||||||
|
print(" Import already present")
|
||||||
|
|
||||||
|
# Add route
|
||||||
|
if '/addon-network' not in content:
|
||||||
|
# Insert before the final closing of server array
|
||||||
|
target = " ],\n} as Routes;"
|
||||||
|
if target in content:
|
||||||
|
content = content.replace(target, f"{ROUTE_BLOCK},\n ],\n}} as Routes;")
|
||||||
|
print(" Added /addon-network route")
|
||||||
|
else:
|
||||||
|
print(" WARN: routes.ts closing not found - trying alternate")
|
||||||
|
# Try without the extra comma
|
||||||
|
target2 = " ]\n} as Routes;"
|
||||||
|
if target2 in content:
|
||||||
|
content = content.replace(target2, f"{ROUTE_BLOCK},\n ]\n}} as Routes;")
|
||||||
|
print(" Added /addon-network route (alt)")
|
||||||
|
else:
|
||||||
|
print(" Route already present")
|
||||||
|
|
||||||
|
open(rts, 'w').write(content)
|
||||||
|
|
||||||
|
# ── 3. Clean ServerRouter.tsx of any stray addon imports ─────────────────────
|
||||||
|
print("\n=== Cleaning ServerRouter.tsx ===")
|
||||||
|
sr = f'{PANEL}/resources/scripts/routers/ServerRouter.tsx'
|
||||||
|
content = open(sr).read()
|
||||||
|
stray = "import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';\n"
|
||||||
|
if stray in content:
|
||||||
|
content = content.replace(stray, '')
|
||||||
|
open(sr, 'w').write(content)
|
||||||
|
print(" Removed stray import")
|
||||||
|
else:
|
||||||
|
print(" Clean already")
|
||||||
|
|
||||||
|
# ── 4. Remove non-existent middleware from routes ─────────────────────────────
|
||||||
|
print("\n=== Fixing addon route middleware ===")
|
||||||
|
for route_file in ['addon-client.php', 'addon-application.php']:
|
||||||
|
path = f'{PANEL}/routes/{route_file}'
|
||||||
|
content = open(path).read()
|
||||||
|
# Replace named throttle limiters that aren't registered with standard ones
|
||||||
|
import re
|
||||||
|
# Remove ->middleware('throttle:addon-*') chains that use unregistered limiters
|
||||||
|
content = re.sub(r"->middleware\('throttle:addon-[^']+'\)", "", content)
|
||||||
|
# Remove ->middleware('addon.ratelimit')
|
||||||
|
content = re.sub(r",?\s*'addon\.ratelimit'", "", content)
|
||||||
|
open(path, 'w').write(content)
|
||||||
|
print(f" Cleaned {route_file}")
|
||||||
|
|
||||||
|
print("\nAll patches applied. Rebuild the frontend next.")
|
||||||
Reference in New Issue
Block a user