File transfer
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @author Ball Studios <https://git.balls.studio>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Pterodactyl\Addons\AdvancedAdmin\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Http\Requests\Roles\CreateRoleRequest;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Http\Requests\Roles\UpdateRoleRequest;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Http\Resources\RoleResource;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Models\Role;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Services\Roles\RoleService;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
public function __construct(private readonly RoleService $roleService) {}
|
||||
|
||||
/** GET /api/application/addons/roles */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorizeAdmin($request);
|
||||
|
||||
$roles = Role::with('permissions')
|
||||
->orderByDesc('priority')
|
||||
->paginate(50);
|
||||
|
||||
return RoleResource::collection($roles)->response();
|
||||
}
|
||||
|
||||
/** GET /api/application/addons/roles/{role} */
|
||||
public function show(Request $request, Role $role): JsonResponse
|
||||
{
|
||||
$this->authorizeAdmin($request);
|
||||
return (new RoleResource($role->load('permissions')))->response();
|
||||
}
|
||||
|
||||
/** POST /api/application/addons/roles */
|
||||
public function store(CreateRoleRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorizeAdmin($request);
|
||||
$role = $this->roleService->create($request->validated(), $request->user()->id);
|
||||
return (new RoleResource($role->load('permissions')))->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
/** PATCH /api/application/addons/roles/{role} */
|
||||
public function update(UpdateRoleRequest $request, Role $role): JsonResponse
|
||||
{
|
||||
$this->authorizeAdmin($request);
|
||||
$updated = $this->roleService->update($role, $request->validated(), $request->user()->id);
|
||||
return (new RoleResource($updated->load('permissions')))->response();
|
||||
}
|
||||
|
||||
/** DELETE /api/application/addons/roles/{role} */
|
||||
public function destroy(Request $request, Role $role): JsonResponse
|
||||
{
|
||||
$this->authorizeAdmin($request);
|
||||
$force = (bool) $request->boolean('force', false);
|
||||
$this->roleService->delete($role, $request->user()->id, $force);
|
||||
return response()->json(['deleted' => true]);
|
||||
}
|
||||
|
||||
private function authorizeAdmin(Request $request): void
|
||||
{
|
||||
abort_unless($request->user() && $request->user()->root_admin, 403, 'Admin access required.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @author Ball Studios <https://git.balls.studio>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Pterodactyl\Addons\AdvancedAdmin\Http\Controllers\Client;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Models\ConsolePlayerCache;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Services\Console\AutocompleteEngine;
|
||||
use Pterodactyl\Models\Server;
|
||||
|
||||
class AutocompleteController extends Controller
|
||||
{
|
||||
public function __construct(private readonly AutocompleteEngine $engine) {}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/console/autocomplete?q= */
|
||||
public function suggest(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->requireAccess($request, $server);
|
||||
|
||||
$query = (string) $request->get('q', '');
|
||||
$suggestions = $this->engine->suggest(
|
||||
$server->id,
|
||||
$server->node_id,
|
||||
$server->egg_id,
|
||||
$query,
|
||||
);
|
||||
|
||||
return response()->json(['data' => $suggestions]);
|
||||
}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/console/players */
|
||||
public function players(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->requireAccess($request, $server);
|
||||
|
||||
$players = ConsolePlayerCache::query()
|
||||
->where('server_id', $server->id)
|
||||
->where('last_seen_at', '>=', now()->subMinutes(
|
||||
config('advanced-admin.console.player_cache_ttl_minutes', 30)
|
||||
))
|
||||
->orderBy('player_name')
|
||||
->pluck('player_name');
|
||||
|
||||
return response()->json(['data' => $players]);
|
||||
}
|
||||
|
||||
private function requireAccess(Request $request, Server $server): void
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_unless($user, 401);
|
||||
|
||||
$isOwner = $server->owner_id === $user->id;
|
||||
$isAdmin = $user->root_admin;
|
||||
|
||||
if (!$isOwner && !$isAdmin) {
|
||||
$subuser = $server->subusers()->where('user_id', $user->id)->first();
|
||||
abort_unless($subuser && in_array('control.console', $subuser->permissions ?? [], true), 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* File Revision Controller — client API
|
||||
*
|
||||
* @author Ball Studios <https://git.balls.studio>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Pterodactyl\Addons\AdvancedAdmin\Http\Controllers\Client;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Models\FileRevision;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Services\Revisions\DiffService;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Services\Revisions\RevisionService;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Services\Revisions\RevisionStorageService;
|
||||
use Pterodactyl\Models\Server;
|
||||
|
||||
class FileRevisionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RevisionService $revisionService,
|
||||
private readonly DiffService $diffService,
|
||||
private readonly RevisionStorageService $storage,
|
||||
) {}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/files/revisions?path= */
|
||||
public function index(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->requirePermission($request, $server, 'file.read');
|
||||
|
||||
$path = $request->string('path', '')->toString();
|
||||
abort_if(empty($path), 422, 'path parameter is required.');
|
||||
|
||||
$path = $this->storage->guardFilePath($path);
|
||||
$pathHash = $this->storage->pathHash($server->id, $path);
|
||||
|
||||
$revisions = FileRevision::query()
|
||||
->where('server_id', $server->id)
|
||||
->where('path_hash', $pathHash)
|
||||
->with('author:id,username,email')
|
||||
->orderByDesc('revision_number')
|
||||
->paginate(25);
|
||||
|
||||
return response()->json($revisions);
|
||||
}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/files/revisions/{revision} */
|
||||
public function show(Request $request, Server $server, FileRevision $revision): JsonResponse
|
||||
{
|
||||
$this->requirePermission($request, $server, 'file.read');
|
||||
$this->assertBelongsToServer($revision, $server);
|
||||
|
||||
return response()->json(['data' => $revision->load('author:id,username,email')]);
|
||||
}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/files/revisions/{revision}/diff */
|
||||
public function diff(Request $request, Server $server, FileRevision $revision): JsonResponse
|
||||
{
|
||||
$this->requirePermission($request, $server, 'file.read');
|
||||
$this->assertBelongsToServer($revision, $server);
|
||||
|
||||
$content = $this->storage->retrieve($revision->storage_key);
|
||||
|
||||
// If binary, return a placeholder diff
|
||||
if ($this->storage->isBinary($content)) {
|
||||
return response()->json(['binary' => true, 'lines' => []]);
|
||||
}
|
||||
|
||||
// Fetch previous revision for comparison
|
||||
$previous = FileRevision::query()
|
||||
->where('server_id', $server->id)
|
||||
->where('path_hash', $revision->path_hash)
|
||||
->where('revision_number', $revision->revision_number - 1)
|
||||
->first();
|
||||
|
||||
$oldContent = $previous ? $this->storage->retrieve($previous->storage_key) : '';
|
||||
|
||||
$lines = $this->diffService->diff($oldContent, $content, basename($revision->file_path));
|
||||
return response()->json(['binary' => false, 'lines' => $lines]);
|
||||
}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/files/revisions/{revision}/download */
|
||||
public function download(Request $request, Server $server, FileRevision $revision): StreamedResponse
|
||||
{
|
||||
$this->requirePermission($request, $server, 'file.read');
|
||||
$this->assertBelongsToServer($revision, $server);
|
||||
|
||||
$content = $this->storage->retrieve($revision->storage_key);
|
||||
$filename = basename($revision->file_path) . ".rev{$revision->revision_number}";
|
||||
|
||||
return response()->streamDownload(function () use ($content) {
|
||||
echo $content;
|
||||
}, $filename, ['Content-Type' => 'application/octet-stream']);
|
||||
}
|
||||
|
||||
/** POST /api/client/servers/{server}/addons/files/revisions/{revision}/restore */
|
||||
public function restore(Request $request, Server $server, FileRevision $revision): JsonResponse
|
||||
{
|
||||
$this->requirePermission($request, $server, 'file.update');
|
||||
$this->assertBelongsToServer($revision, $server);
|
||||
|
||||
$content = $this->revisionService->restore($revision, $request->user()->id);
|
||||
|
||||
return response()->json([
|
||||
'restored' => true,
|
||||
'content' => $content,
|
||||
'revision' => $revision->revision_number,
|
||||
]);
|
||||
}
|
||||
|
||||
/** DELETE /api/client/servers/{server}/addons/files/revisions/{revision} */
|
||||
public function destroy(Request $request, Server $server, FileRevision $revision): JsonResponse
|
||||
{
|
||||
// Only admins can manually delete revisions
|
||||
abort_unless($request->user()?->root_admin, 403, 'Admin access required.');
|
||||
$this->assertBelongsToServer($revision, $server);
|
||||
|
||||
$this->revisionService->delete($revision);
|
||||
return response()->json(['deleted' => true]);
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private function requirePermission(Request $request, Server $server, string $permission): void
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_unless($user, 401);
|
||||
|
||||
$isOwner = $server->owner_id === $user->id;
|
||||
$isAdmin = $user->root_admin;
|
||||
|
||||
if (!$isOwner && !$isAdmin) {
|
||||
$subuser = $server->subusers()->where('user_id', $user->id)->first();
|
||||
abort_unless(
|
||||
$subuser && in_array($permission, $subuser->permissions ?? [], true),
|
||||
403,
|
||||
"Permission '{$permission}' required."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function assertBelongsToServer(FileRevision $revision, Server $server): void
|
||||
{
|
||||
abort_unless($revision->server_id === $server->id, 404);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Network Metrics Controller — client API
|
||||
*
|
||||
* @author Ball Studios <https://git.balls.studio>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Pterodactyl\Addons\AdvancedAdmin\Http\Controllers\Client;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Pterodactyl\Models\Server;
|
||||
|
||||
class NetworkMetricsController extends Controller
|
||||
{
|
||||
/** GET /api/client/servers/{server}/addons/network/live */
|
||||
public function live(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->requireAccess($request, $server);
|
||||
|
||||
// Latest raw metric (most recent 30 seconds)
|
||||
$latest = DB::table('addon_network_metrics')
|
||||
->where('server_id', $server->id)
|
||||
->where('resolution', 'raw')
|
||||
->where('recorded_at', '>=', now()->subSeconds(30))
|
||||
->orderByDesc('recorded_at')
|
||||
->select('rx_bytes', 'tx_bytes', 'recorded_at')
|
||||
->first();
|
||||
|
||||
return response()->json(['data' => $latest]);
|
||||
}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/network/history */
|
||||
public function history(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->requireAccess($request, $server);
|
||||
|
||||
$resolution = $request->get('resolution', '5m');
|
||||
abort_unless(in_array($resolution, ['raw', '5m', '1h', '1d'], true), 422, 'Invalid resolution.');
|
||||
|
||||
$hours = match ($resolution) {
|
||||
'raw' => 1,
|
||||
'5m' => 24,
|
||||
'1h' => 168,
|
||||
'1d' => 8760,
|
||||
};
|
||||
|
||||
$metrics = DB::table('addon_network_metrics')
|
||||
->where('server_id', $server->id)
|
||||
->where('resolution', $resolution)
|
||||
->where('recorded_at', '>=', now()->subHours($hours))
|
||||
->orderBy('recorded_at')
|
||||
->select('rx_bytes', 'tx_bytes', 'recorded_at')
|
||||
->get();
|
||||
|
||||
return response()->json(['data' => $metrics]);
|
||||
}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/network/ports */
|
||||
public function ports(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->requireAccess($request, $server);
|
||||
|
||||
if (!config('advanced-admin.network.iptables_enabled')) {
|
||||
return response()->json(['data' => [], 'unavailable' => true, 'reason' => 'iptables collection not enabled']);
|
||||
}
|
||||
|
||||
$ports = DB::table('addon_network_port_metrics')
|
||||
->where('server_id', $server->id)
|
||||
->where('resolution', '5m')
|
||||
->where('recorded_at', '>=', now()->subHour())
|
||||
->groupBy('port', 'protocol')
|
||||
->select('port', 'protocol', DB::raw('SUM(rx_bytes) as rx_bytes'), DB::raw('SUM(tx_bytes) as tx_bytes'))
|
||||
->orderByDesc('rx_bytes')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return response()->json(['data' => $ports]);
|
||||
}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/network/flows */
|
||||
public function flows(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->requireAccess($request, $server);
|
||||
|
||||
$flows = DB::table('addon_network_flows')
|
||||
->where(function ($q) use ($server) {
|
||||
$q->where('src_server_id', $server->id)
|
||||
->orWhere('dst_server_id', $server->id);
|
||||
})
|
||||
->where('recorded_at', '>=', now()->subHours(24))
|
||||
->select('src_server_id', 'dst_server_id', DB::raw('SUM(bytes) as bytes'), DB::raw('SUM(packets) as packets'))
|
||||
->groupBy('src_server_id', 'dst_server_id')
|
||||
->orderByDesc('bytes')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return response()->json(['data' => $flows]);
|
||||
}
|
||||
|
||||
private function requireAccess(Request $request, Server $server): void
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_unless($user, 401);
|
||||
$isOwner = $server->owner_id === $user->id;
|
||||
$isAdmin = $user->root_admin;
|
||||
if (!$isOwner && !$isAdmin) {
|
||||
$subuser = $server->subusers()->where('user_id', $user->id)->first();
|
||||
abort_unless($subuser, 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @author Ball Studios <https://git.balls.studio>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Pterodactyl\Addons\AdvancedAdmin\Http\Controllers\Client;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Http\Resources\RoleResource;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Http\Resources\EffectivePermissionResource;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Models\ServerRoleAssignment;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Services\Roles\PermissionResolver;
|
||||
use Pterodactyl\Addons\AdvancedAdmin\Services\Roles\RoleService;
|
||||
use Pterodactyl\Models\Server;
|
||||
|
||||
class ServerRoleController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoleService $roleService,
|
||||
private readonly PermissionResolver $resolver,
|
||||
) {}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/roles/users */
|
||||
public function listUsers(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->authorizeServerAdmin($request, $server);
|
||||
|
||||
$assignments = ServerRoleAssignment::query()
|
||||
->where('server_id', $server->id)
|
||||
->with(['user', 'role.permissions'])
|
||||
->get()
|
||||
->groupBy('user_id')
|
||||
->map(function ($items) {
|
||||
$user = $items->first()->user;
|
||||
$roles = $items->pluck('role');
|
||||
return ['user' => $user, 'roles' => $roles];
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json(['data' => $assignments]);
|
||||
}
|
||||
|
||||
/** POST /api/client/servers/{server}/addons/roles/assign */
|
||||
public function assign(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->authorizeServerAdmin($request, $server);
|
||||
|
||||
$data = $request->validate([
|
||||
'user_id' => 'required|integer|exists:users,id',
|
||||
'role_id' => 'required|integer|exists:addon_roles,id',
|
||||
]);
|
||||
|
||||
$assignment = $this->roleService->assignToUser(
|
||||
$server->id,
|
||||
$data['user_id'],
|
||||
$data['role_id'],
|
||||
$request->user()->id,
|
||||
);
|
||||
|
||||
return response()->json(['data' => $assignment], 201);
|
||||
}
|
||||
|
||||
/** DELETE /api/client/servers/{server}/addons/roles/assign */
|
||||
public function unassign(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->authorizeServerAdmin($request, $server);
|
||||
|
||||
$data = $request->validate([
|
||||
'user_id' => 'required|integer|exists:users,id',
|
||||
'role_id' => 'required|integer|exists:addon_roles,id',
|
||||
]);
|
||||
|
||||
$this->roleService->unassignFromUser(
|
||||
$server->id,
|
||||
$data['user_id'],
|
||||
$data['role_id'],
|
||||
$request->user()->id,
|
||||
);
|
||||
|
||||
return response()->json(['deleted' => true]);
|
||||
}
|
||||
|
||||
/** GET /api/client/servers/{server}/addons/permissions/effective/{userId} */
|
||||
public function effectivePermissions(Request $request, Server $server, int $targetUserId): JsonResponse
|
||||
{
|
||||
$this->authorizeServerAdmin($request, $server);
|
||||
|
||||
$effective = $this->resolver->resolveAll(
|
||||
$targetUserId,
|
||||
$server->id,
|
||||
RoleService::KNOWN_PERMISSIONS
|
||||
);
|
||||
|
||||
return response()->json(['data' => $effective]);
|
||||
}
|
||||
|
||||
/** PUT /api/client/servers/{server}/addons/permissions/override */
|
||||
public function setOverride(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->authorizeServerAdmin($request, $server);
|
||||
|
||||
$data = $request->validate([
|
||||
'user_id' => 'required|integer|exists:users,id',
|
||||
'permission' => 'required|string|max:100',
|
||||
'value' => 'required|in:allow,deny,unset',
|
||||
]);
|
||||
|
||||
$override = $this->roleService->setOverride(
|
||||
$server->id,
|
||||
$data['user_id'],
|
||||
$data['permission'],
|
||||
$data['value'],
|
||||
$request->user()->id,
|
||||
);
|
||||
|
||||
return response()->json(['data' => $override]);
|
||||
}
|
||||
|
||||
private function authorizeServerAdmin(Request $request, Server $server): void
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_unless($user, 401);
|
||||
|
||||
$isOwner = $server->owner_id === $user->id;
|
||||
$isAdmin = $user->root_admin;
|
||||
|
||||
// Subusers with user.update can also manage roles on their server
|
||||
$isSubuserAdmin = false;
|
||||
if (!$isOwner && !$isAdmin) {
|
||||
$subuser = $server->subusers()->where('user_id', $user->id)->first();
|
||||
$isSubuserAdmin = $subuser && in_array('user.update', $subuser->permissions ?? [], true);
|
||||
}
|
||||
|
||||
abort_unless($isOwner || $isAdmin || $isSubuserAdmin, 403, 'Insufficient privileges.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user