From 3236acab168e602ffc37575da4757a8f922ac825 Mon Sep 17 00:00:00 2001 From: BaileyCodes Date: Thu, 11 Jun 2026 21:29:54 -0500 Subject: [PATCH] feat: add RolesPage.tsx, patch_all_modules.py for all 4 addon modules --- .../addons/advanced-admin/roles/RolesPage.tsx | 102 +++++++++ scripts/patch_all_modules.py | 197 ++++++++++++++++++ scripts/patch_routes.py | 87 ++++++++ scripts/repatch.py | 97 +++++++++ 4 files changed, 483 insertions(+) create mode 100644 resources/scripts/addons/advanced-admin/roles/RolesPage.tsx create mode 100644 scripts/patch_all_modules.py create mode 100644 scripts/patch_routes.py create mode 100644 scripts/repatch.py diff --git a/resources/scripts/addons/advanced-admin/roles/RolesPage.tsx b/resources/scripts/addons/advanced-admin/roles/RolesPage.tsx new file mode 100644 index 0000000..5524d04 --- /dev/null +++ b/resources/scripts/addons/advanced-admin/roles/RolesPage.tsx @@ -0,0 +1,102 @@ +/** + * Roles Management Page β€” Admin view for role-based permissions + * Ball Studios + */ + +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([]); + 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 ( + +
+

πŸ›‘ Role Permissions

+

+ Your effective permissions for this server, managed by the Ball Studios Advanced Admin addon. +

+
+ + {loading && ( +
Loading permissions…
+ )} + + {error && ( +
+ {error} +
+ )} + + {!loading && !error && userRoles.length === 0 && ( +
+ No roles assigned. Contact an administrator. +
+ )} + +
+ {userRoles.map((ur, i) => ( +
+
+ + + {ur.role?.name ?? 'Unknown Role'} + + {ur.source && ( + + via {ur.source} + + )} +
+ {ur.role?.description && ( +

{ur.role.description}

+ )} + {ur.role?.permissions?.length > 0 && ( +
+ {ur.role.permissions.map((perm: string) => ( + + {perm} + + ))} +
+ )} +
+ ))} +
+
+ ); +} diff --git a/scripts/patch_all_modules.py b/scripts/patch_all_modules.py new file mode 100644 index 0000000..dbf5d07 --- /dev/null +++ b/scripts/patch_all_modules.py @@ -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 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', + '", + "
\n {action === 'edit' && }" +) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# 3. Console.tsx β€” inject CommandInput wrapper +# +# The console's existing handles history/keyboard; our CommandInput +# is a self-contained replacement that renders its own + 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 ' { + setHistory((prev: string[]) => [cmd, ...(prev || [])].slice(0, 32)); + instance && instance.send('send command', cmd); + }} + disabled={!connected} + /> + {/* native input kept for history arrow-key navigation */} + { instance && instance.send('send command', cmd); }} + disabled={!connected} + /> + 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.")