diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 6fd1b5d8e..4cfee8e46 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -233,7 +233,7 @@ final class Handler extends ExceptionHandler /** * Return an array of exceptions that should not be reported. */ - public static function isReportable(\Exception $exception): bool + public static function isReportable(Exception $exception): bool { return (new static(Container::getInstance()))->shouldReport($exception); } diff --git a/app/Exceptions/Http/QueryValueOutOfRangeHttpException.php b/app/Exceptions/Http/QueryValueOutOfRangeHttpException.php new file mode 100644 index 000000000..4610f0e2e --- /dev/null +++ b/app/Exceptions/Http/QueryValueOutOfRangeHttpException.php @@ -0,0 +1,21 @@ + 'https://github.com/pterodactyl/panel/blob/develop/package.json', - ]; - } -} diff --git a/app/Exceptions/Transformer/InvalidTransformerLevelException.php b/app/Exceptions/Transformer/InvalidTransformerLevelException.php deleted file mode 100644 index 3d4c24248..000000000 --- a/app/Exceptions/Transformer/InvalidTransformerLevelException.php +++ /dev/null @@ -1,9 +0,0 @@ - $abstract - * - * @return T - * - * @noinspection PhpDocSignatureInspection + * Return an HTTP/201 response for the API. */ - public function getTransformer(string $abstract) + protected function returnAccepted(): Response { - Assert::subclassOf($abstract, BaseTransformer::class); - - return $abstract::fromRequest($this->request); + return new Response('', Response::HTTP_ACCEPTED); } /** diff --git a/app/Http/Controllers/Api/Application/Databases/DatabaseController.php b/app/Http/Controllers/Api/Application/Databases/DatabaseController.php new file mode 100644 index 000000000..2acd2fa18 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Databases/DatabaseController.php @@ -0,0 +1,99 @@ +query('per_page', '10'); + if ($perPage < 1 || $perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + + $databases = QueryBuilder::for(DatabaseHost::query()) + ->allowedFilters(['name', 'host']) + ->allowedSorts(['id', 'name', 'host']) + ->paginate($perPage); + + return $this->fractal->collection($databases) + ->transformWith(DatabaseHostTransformer::class) + ->toArray(); + } + + /** + * Returns a single database host. + */ + public function view(GetDatabaseRequest $request, DatabaseHost $databaseHost): array + { + return $this->fractal->item($databaseHost) + ->transformWith(DatabaseHostTransformer::class) + ->toArray(); + } + + /** + * Creates a new database host. + * + * @throws \Throwable + */ + public function store(StoreDatabaseRequest $request): JsonResponse + { + $databaseHost = $this->creationService->handle($request->validated()); + + return $this->fractal->item($databaseHost) + ->transformWith(DatabaseHostTransformer::class) + ->respond(JsonResponse::HTTP_CREATED); + } + + /** + * Updates a database host. + * + * @throws \Throwable + */ + public function update(UpdateDatabaseRequest $request, DatabaseHost $databaseHost): array + { + $databaseHost = $this->updateService->handle($databaseHost->id, $request->validated()); + + return $this->fractal->item($databaseHost) + ->transformWith(DatabaseHostTransformer::class) + ->toArray(); + } + + /** + * Deletes a database host. + * + * @throws \Exception + */ + public function delete(DeleteDatabaseRequest $request, DatabaseHost $databaseHost): Response + { + $databaseHost->delete(); + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Controllers/Api/Application/Eggs/EggController.php b/app/Http/Controllers/Api/Application/Eggs/EggController.php new file mode 100644 index 000000000..96c0b8a4f --- /dev/null +++ b/app/Http/Controllers/Api/Application/Eggs/EggController.php @@ -0,0 +1,120 @@ +eggExporterService = $eggExporterService; + } + + /** + * Return an array of all eggs on a given nest. + */ + public function index(GetEggsRequest $request, Nest $nest): array + { + $perPage = (int) $request->query('per_page', '10'); + if ($perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + + // @phpstan-ignore-next-line + $eggs = QueryBuilder::for(Egg::query()) + ->where('nest_id', '=', $nest->id) + ->allowedFilters(['id', 'name', 'author']) + ->allowedSorts(['id', 'name', 'author']); + if ($perPage > 0) { + $eggs = $eggs->paginate($perPage); + } + + return $this->fractal->collection($eggs) + ->transformWith(EggTransformer::class) + ->toArray(); + } + + /** + * Returns a single egg. + */ + public function view(GetEggRequest $request, Egg $egg): array + { + return $this->fractal->item($egg) + ->transformWith(EggTransformer::class) + ->toArray(); + } + + /** + * Creates a new egg. + */ + public function store(StoreEggRequest $request): JsonResponse + { + $validated = $request->validated(); + $merged = array_merge($validated, [ + 'uuid' => Uuid::uuid4()->toString(), + // TODO: allow this to be set in the request, and default to config value if null or not present. + 'author' => config('pterodactyl.service.author'), + ]); + + $egg = Egg::query()->create($merged); + + return $this->fractal->item($egg) + ->transformWith(EggTransformer::class) + ->respond(Response::HTTP_CREATED); + } + + /** + * Updates an egg. + */ + public function update(UpdateEggRequest $request, Egg $egg): array + { + $egg->update($request->validated()); + + return $this->fractal->item($egg) + ->transformWith(EggTransformer::class) + ->toArray(); + } + + /** + * Deletes an egg. + * + * @throws \Exception + */ + public function delete(DeleteEggRequest $request, Egg $egg): Response + { + $egg->delete(); + + return $this->returnNoContent(); + } + + /** + * Exports an egg. + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function export(ExportEggRequest $request, int $eggId): JsonResponse + { + return new JsonResponse($this->eggExporterService->handle($eggId)); + } +} diff --git a/app/Http/Controllers/Api/Application/Eggs/EggVariableController.php b/app/Http/Controllers/Api/Application/Eggs/EggVariableController.php new file mode 100644 index 000000000..c837244d7 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Eggs/EggVariableController.php @@ -0,0 +1,75 @@ +variableCreationService->handle($egg->id, $request->validated()); + + return $this->fractal->item($variable) + ->transformWith(EggVariableTransformer::class) + ->toArray(); + } + + /** + * Updates multiple egg variables. + * + * @throws \Throwable + */ + public function update(UpdateEggVariablesRequest $request, Egg $egg): array + { + $validated = $request->validated(); + + $this->connection->transaction(function () use ($egg, $validated) { + foreach ($validated as $data) { + $this->variableUpdateService->handle($egg, $data); + } + }); + + return $this->fractal->collection($egg->refresh()->variables) + ->transformWith(EggVariableTransformer::class) + ->toArray(); + } + + /** + * Deletes a single egg variable. + */ + public function delete(Request $request, Egg $egg, EggVariable $eggVariable): Response + { + EggVariable::query() + ->where('id', $eggVariable->id) + ->where('egg_id', $egg->id) + ->delete(); + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Controllers/Api/Application/Locations/LocationController.php b/app/Http/Controllers/Api/Application/Locations/LocationController.php index b95a0776a..337f5e76d 100644 --- a/app/Http/Controllers/Api/Application/Locations/LocationController.php +++ b/app/Http/Controllers/Api/Application/Locations/LocationController.php @@ -10,6 +10,7 @@ use Pterodactyl\Services\Locations\LocationUpdateService; use Pterodactyl\Services\Locations\LocationCreationService; use Pterodactyl\Services\Locations\LocationDeletionService; use Pterodactyl\Transformers\Api\Application\LocationTransformer; +use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationsRequest; @@ -35,13 +36,18 @@ class LocationController extends ApplicationApiController */ public function index(GetLocationsRequest $request): array { + $perPage = (int) $request->query('per_page', '10'); + if ($perPage < 1 || $perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + $locations = QueryBuilder::for(Location::query()) ->allowedFilters(['short', 'long']) - ->allowedSorts(['id']) - ->paginate($request->query('per_page') ?? 50); + ->allowedSorts(['id', 'short', 'long']) + ->paginate($perPage); return $this->fractal->collection($locations) - ->transformWith($this->getTransformer(LocationTransformer::class)) + ->transformWith(LocationTransformer::class) ->toArray(); } @@ -51,7 +57,7 @@ class LocationController extends ApplicationApiController public function view(GetLocationRequest $request, Location $location): array { return $this->fractal->item($location) - ->transformWith($this->getTransformer(LocationTransformer::class)) + ->transformWith(LocationTransformer::class) ->toArray(); } @@ -66,12 +72,7 @@ class LocationController extends ApplicationApiController $location = $this->creationService->handle($request->validated()); return $this->fractal->item($location) - ->transformWith($this->getTransformer(LocationTransformer::class)) - ->addMeta([ - 'resource' => route('api.application.locations.view', [ - 'location' => $location->id, - ]), - ]) + ->transformWith(LocationTransformer::class) ->respond(201); } @@ -86,7 +87,7 @@ class LocationController extends ApplicationApiController $location = $this->updateService->handle($location, $request->validated()); return $this->fractal->item($location) - ->transformWith($this->getTransformer(LocationTransformer::class)) + ->transformWith(LocationTransformer::class) ->toArray(); } @@ -99,6 +100,6 @@ class LocationController extends ApplicationApiController { $this->deletionService->handle($location); - return response('', 204); + return $this->returnNoContent(); } } diff --git a/app/Http/Controllers/Api/Application/Mounts/MountController.php b/app/Http/Controllers/Api/Application/Mounts/MountController.php new file mode 100644 index 000000000..6606004fd --- /dev/null +++ b/app/Http/Controllers/Api/Application/Mounts/MountController.php @@ -0,0 +1,163 @@ +query('per_page', '10'); + if ($perPage < 1 || $perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + + $mounts = QueryBuilder::for(Mount::query()) + ->allowedFilters(['id', 'name', 'source', 'target']) + ->allowedSorts(['id', 'name', 'source', 'target']) + ->paginate($perPage); + + return $this->fractal->collection($mounts) + ->transformWith(MountTransformer::class) + ->toArray(); + } + + /** + * Returns a single mount. + */ + public function view(GetMountRequest $request, Mount $mount): array + { + return $this->fractal->item($mount) + ->transformWith(MountTransformer::class) + ->toArray(); + } + + /** + * Creates a new mount. + */ + public function store(StoreMountRequest $request): JsonResponse + { + $mount = Mount::query()->create($request->validated()); + + return $this->fractal->item($mount) + ->transformWith(MountTransformer::class) + ->respond(JsonResponse::HTTP_CREATED); + } + + /** + * Updates a mount. + */ + public function update(UpdateMountRequest $request, Mount $mount): array + { + $mount->update($request->validated()); + + return $this->fractal->item($mount) + ->transformWith(MountTransformer::class) + ->toArray(); + } + + /** + * Deletes a mount. + * + * @throws \Exception + */ + public function delete(DeleteMountRequest $request, Mount $mount): Response + { + $mount->delete(); + + return $this->returnNoContent(); + } + + /** + * Attaches eggs to a mount. + */ + public function addEggs(MountEggsRequest $request, Mount $mount): array + { + $data = $request->validated(); + + $eggs = $data['eggs'] ?? []; + if (count($eggs) > 0) { + $mount->eggs()->syncWithoutDetaching($eggs); + } + + return $this->fractal->item($mount) + ->transformWith(MountTransformer::class) + ->toArray(); + } + + /** + * Attaches nodes to a mount. + */ + public function addNodes(MountNodesRequest $request, Mount $mount): array + { + $data = $request->validated(); + + $nodes = $data['nodes'] ?? []; + if (count($nodes) > 0) { + $mount->nodes()->syncWithoutDetaching($nodes); + } + + return $this->fractal->item($mount) + ->transformWith(MountTransformer::class) + ->toArray(); + } + + /** + * Detaches eggs from a mount. + */ + public function deleteEggs(MountEggsRequest $request, Mount $mount): array + { + $data = $request->validated(); + + $eggs = $data['eggs'] ?? []; + if (count($eggs) > 0) { + $mount->eggs()->detach($eggs); + } + + return $this->fractal->item($mount) + ->transformWith(MountTransformer::class) + ->toArray(); + } + + /** + * Detaches nodes from a mount. + */ + public function deleteNodes(MountNodesRequest $request, Mount $mount): array + { + $data = $request->validated(); + + $nodes = $data['nodes'] ?? []; + if (count($nodes) > 0) { + $mount->nodes()->detach($nodes); + } + + return $this->fractal->item($mount) + ->transformWith(MountTransformer::class) + ->toArray(); + } +} diff --git a/app/Http/Controllers/Api/Application/Nests/EggController.php b/app/Http/Controllers/Api/Application/Nests/EggController.php deleted file mode 100644 index 83c3f77a0..000000000 --- a/app/Http/Controllers/Api/Application/Nests/EggController.php +++ /dev/null @@ -1,33 +0,0 @@ -fractal->collection($nest->eggs) - ->transformWith($this->getTransformer(EggTransformer::class)) - ->toArray(); - } - - /** - * Return a single egg that exists on the specified nest. - */ - public function view(GetEggRequest $request, Nest $nest, Egg $egg): array - { - return $this->fractal->item($egg) - ->transformWith($this->getTransformer(EggTransformer::class)) - ->toArray(); - } -} diff --git a/app/Http/Controllers/Api/Application/Nests/NestController.php b/app/Http/Controllers/Api/Application/Nests/NestController.php index f0044f53c..708f59195 100644 --- a/app/Http/Controllers/Api/Application/Nests/NestController.php +++ b/app/Http/Controllers/Api/Application/Nests/NestController.php @@ -3,9 +3,21 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Nests; use Pterodactyl\Models\Nest; -use Pterodactyl\Contracts\Repository\NestRepositoryInterface; +use Illuminate\Http\Response; +use Spatie\QueryBuilder\QueryBuilder; +use Pterodactyl\Services\Nests\NestUpdateService; +use Pterodactyl\Services\Nests\NestCreationService; +use Pterodactyl\Services\Nests\NestDeletionService; +use Pterodactyl\Services\Eggs\Sharing\EggImporterService; +use Pterodactyl\Transformers\Api\Application\EggTransformer; use Pterodactyl\Transformers\Api\Application\NestTransformer; +use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException; +use Pterodactyl\Http\Requests\Api\Application\Nests\GetNestRequest; +use Pterodactyl\Http\Requests\Api\Application\Eggs\ImportEggRequest; use Pterodactyl\Http\Requests\Api\Application\Nests\GetNestsRequest; +use Pterodactyl\Http\Requests\Api\Application\Nests\StoreNestRequest; +use Pterodactyl\Http\Requests\Api\Application\Nests\DeleteNestRequest; +use Pterodactyl\Http\Requests\Api\Application\Nests\UpdateNestRequest; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; class NestController extends ApplicationApiController @@ -13,8 +25,12 @@ class NestController extends ApplicationApiController /** * NestController constructor. */ - public function __construct(private NestRepositoryInterface $repository) - { + public function __construct( + private NestCreationService $nestCreationService, + private NestDeletionService $nestDeletionService, + private NestUpdateService $nestUpdateService, + private EggImporterService $eggImporterService + ) { parent::__construct(); } @@ -23,20 +39,87 @@ class NestController extends ApplicationApiController */ public function index(GetNestsRequest $request): array { - $nests = $this->repository->paginated($request->query('per_page') ?? 50); + $perPage = (int) $request->query('per_page', '10'); + if ($perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + + $nests = QueryBuilder::for(Nest::query()) + ->allowedFilters(['id', 'name', 'author']) + ->allowedSorts(['id', 'name', 'author']); + if ($perPage > 0) { + $nests = $nests->paginate($perPage); + } return $this->fractal->collection($nests) - ->transformWith($this->getTransformer(NestTransformer::class)) + ->transformWith(NestTransformer::class) ->toArray(); } /** * Return information about a single Nest model. */ - public function view(GetNestsRequest $request, Nest $nest): array + public function view(GetNestRequest $request, Nest $nest): array { return $this->fractal->item($nest) - ->transformWith($this->getTransformer(NestTransformer::class)) + ->transformWith(NestTransformer::class) ->toArray(); } + + /** + * Creates a new nest. + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(StoreNestRequest $request): array + { + $nest = $this->nestCreationService->handle($request->validated()); + + return $this->fractal->item($nest) + ->transformWith(NestTransformer::class) + ->toArray(); + } + + /** + * Imports an egg. + */ + public function import(ImportEggRequest $request, Nest $nest): array + { + $egg = $this->eggImporterService->handleContent( + $nest->id, + $request->getContent(), + $request->headers->get('Content-Type'), + ); + + return $this->fractal->item($egg) + ->transformWith(EggTransformer::class) + ->toArray(); + } + + /** + * Updates an existing nest. + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(UpdateNestRequest $request, Nest $nest): array + { + $this->nestUpdateService->handle($nest->id, $request->validated()); + + return $this->fractal->item($nest) + ->transformWith(NestTransformer::class) + ->toArray(); + } + + /** + * Deletes an existing nest. + * + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException + */ + public function delete(DeleteNestRequest $request, Nest $nest): Response + { + $this->nestDeletionService->handle($nest->id); + + return $this->returnNoContent(); + } } diff --git a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php index 77404945a..41483bf41 100644 --- a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php +++ b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php @@ -3,13 +3,14 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Nodes; use Pterodactyl\Models\Node; -use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; use Pterodactyl\Models\Allocation; use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\AllowedFilter; use Illuminate\Database\Eloquent\Builder; use Pterodactyl\Services\Allocations\AssignmentService; use Pterodactyl\Services\Allocations\AllocationDeletionService; +use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException; use Pterodactyl\Transformers\Api\Application\AllocationTransformer; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest; @@ -33,23 +34,27 @@ class AllocationController extends ApplicationApiController */ public function index(GetAllocationsRequest $request, Node $node): array { - $allocations = QueryBuilder::for($node->allocations()) - ->allowedFilters([ - AllowedFilter::exact('ip'), - AllowedFilter::exact('port'), - 'ip_alias', - AllowedFilter::callback('server_id', function (Builder $builder, $value) { - if (empty($value) || is_bool($value) || !ctype_digit((string) $value)) { - return $builder->whereNull('server_id'); - } + $perPage = (int) $request->query('per_page', '10'); + if ($perPage < 1 || $perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } - return $builder->where('server_id', $value); + $allocations = QueryBuilder::for(Allocation::query()->where('node_id', '=', $node->id)) + ->allowedFilters([ + 'id', 'ip', 'port', 'alias', + AllowedFilter::callback('server_id', function (Builder $query, $value) { + if ($value === '0') { + $query->whereNull('server_id'); + } else { + $query->where('server_id', '=', $value); + } }), ]) - ->paginate($request->query('per_page') ?? 50); + ->allowedSorts(['id', 'ip', 'port', 'server_id']) + ->paginate($perPage); return $this->fractal->collection($allocations) - ->transformWith($this->getTransformer(AllocationTransformer::class)) + ->transformWith(AllocationTransformer::class) ->toArray(); } @@ -62,11 +67,11 @@ class AllocationController extends ApplicationApiController * @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException * @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException */ - public function store(StoreAllocationRequest $request, Node $node): JsonResponse + public function store(StoreAllocationRequest $request, Node $node): Response { $this->assignmentService->handle($node, $request->validated()); - return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + return $this->returnNoContent(); } /** @@ -74,10 +79,10 @@ class AllocationController extends ApplicationApiController * * @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException */ - public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): JsonResponse + public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): Response { $this->deletionService->handle($allocation); - return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + return $this->returnNoContent(); } } diff --git a/app/Http/Controllers/Api/Application/Nodes/NodeConfigurationController.php b/app/Http/Controllers/Api/Application/Nodes/NodeConfigurationController.php index 2d812a89b..4709e109a 100644 --- a/app/Http/Controllers/Api/Application/Nodes/NodeConfigurationController.php +++ b/app/Http/Controllers/Api/Application/Nodes/NodeConfigurationController.php @@ -14,8 +14,12 @@ class NodeConfigurationController extends ApplicationApiController * to remote machines so long as an API key is provided to the machine to make the request * with, and the node is known. */ - public function __invoke(GetNodeRequest $request, Node $node): JsonResponse + public function __invoke(GetNodeRequest $request, Node $node): JsonResponse|string { + if ($request->query('format') === 'yaml') { + return $node->getYamlConfiguration(); + } + return new JsonResponse($node->getConfiguration()); } } diff --git a/app/Http/Controllers/Api/Application/Nodes/NodeController.php b/app/Http/Controllers/Api/Application/Nodes/NodeController.php index e0e3575d8..c30272d07 100644 --- a/app/Http/Controllers/Api/Application/Nodes/NodeController.php +++ b/app/Http/Controllers/Api/Application/Nodes/NodeController.php @@ -3,12 +3,14 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Nodes; use Pterodactyl\Models\Node; +use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; use Spatie\QueryBuilder\QueryBuilder; use Pterodactyl\Services\Nodes\NodeUpdateService; use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Services\Nodes\NodeDeletionService; use Pterodactyl\Transformers\Api\Application\NodeTransformer; +use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException; use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodeRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodesRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\StoreNodeRequest; @@ -34,13 +36,18 @@ class NodeController extends ApplicationApiController */ public function index(GetNodesRequest $request): array { + $perPage = (int) $request->query('per_page', '10'); + if ($perPage < 1 || $perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + $nodes = QueryBuilder::for(Node::query()) - ->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id']) - ->allowedSorts(['id', 'uuid', 'memory', 'disk']) - ->paginate($request->query('per_page') ?? 50); + ->allowedFilters(['id', 'uuid', 'name', 'fqdn', 'daemon_token_id']) + ->allowedSorts(['id', 'uuid', 'name', 'location_id', 'fqdn', 'memory', 'disk']) + ->paginate($perPage); return $this->fractal->collection($nodes) - ->transformWith($this->getTransformer(NodeTransformer::class)) + ->transformWith(NodeTransformer::class) ->toArray(); } @@ -50,7 +57,7 @@ class NodeController extends ApplicationApiController public function view(GetNodeRequest $request, Node $node): array { return $this->fractal->item($node) - ->transformWith($this->getTransformer(NodeTransformer::class)) + ->transformWith(NodeTransformer::class) ->toArray(); } @@ -65,12 +72,7 @@ class NodeController extends ApplicationApiController $node = $this->creationService->handle($request->validated()); return $this->fractal->item($node) - ->transformWith($this->getTransformer(NodeTransformer::class)) - ->addMeta([ - 'resource' => route('api.application.nodes.view', [ - 'node' => $node->id, - ]), - ]) + ->transformWith(NodeTransformer::class) ->respond(201); } @@ -84,11 +86,10 @@ class NodeController extends ApplicationApiController $node = $this->updateService->handle( $node, $request->validated(), - $request->input('reset_secret') === true ); return $this->fractal->item($node) - ->transformWith($this->getTransformer(NodeTransformer::class)) + ->transformWith(NodeTransformer::class) ->toArray(); } @@ -98,10 +99,10 @@ class NodeController extends ApplicationApiController * * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ - public function delete(DeleteNodeRequest $request, Node $node): JsonResponse + public function delete(DeleteNodeRequest $request, Node $node): Response { $this->deletionService->handle($node); - return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + return $this->returnNoContent(); } } diff --git a/app/Http/Controllers/Api/Application/Nodes/NodeDeploymentController.php b/app/Http/Controllers/Api/Application/Nodes/NodeDeploymentController.php index 4b49fa9c6..fa09e9707 100644 --- a/app/Http/Controllers/Api/Application/Nodes/NodeDeploymentController.php +++ b/app/Http/Controllers/Api/Application/Nodes/NodeDeploymentController.php @@ -30,10 +30,10 @@ class NodeDeploymentController extends ApplicationApiController $nodes = $this->viableNodesService->setLocations($data['location_ids'] ?? []) ->setMemory($data['memory']) ->setDisk($data['disk']) - ->handle($request->query('per_page'), $request->query('page')); + ->handle($request->query('per_page'), $request->query('page')); // @phpstan-ignore-line return $this->fractal->collection($nodes) - ->transformWith($this->getTransformer(NodeTransformer::class)) + ->transformWith(NodeTransformer::class) ->toArray(); } } diff --git a/app/Http/Controllers/Api/Application/Nodes/NodeInformationController.php b/app/Http/Controllers/Api/Application/Nodes/NodeInformationController.php new file mode 100644 index 000000000..ff099e15b --- /dev/null +++ b/app/Http/Controllers/Api/Application/Nodes/NodeInformationController.php @@ -0,0 +1,47 @@ +cache + ->tags(['nodes']) + ->remember($node->uuid, Carbon::now()->addSeconds(30), function () use ($node) { + return $this->repository->setNode($node)->getSystemInformation(); + }); + + return new JsonResponse([ + 'version' => $data['version'] ?? null, + 'system' => [ + 'type' => Str::title($data['os'] ?? 'Unknown'), + 'arch' => $data['architecture'] ?? null, + 'release' => $data['kernel_version'] ?? null, + 'cpus' => $data['cpu_count'] ?? null, + ], + ]); + } +} diff --git a/app/Http/Controllers/Api/Application/Roles/RoleController.php b/app/Http/Controllers/Api/Application/Roles/RoleController.php new file mode 100644 index 000000000..21ec404aa --- /dev/null +++ b/app/Http/Controllers/Api/Application/Roles/RoleController.php @@ -0,0 +1,96 @@ +query('per_page', '10'); + if ($perPage < 1 || $perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + + $roles = QueryBuilder::for(AdminRole::query()) + ->allowedFilters(['id', 'name']) + ->allowedSorts(['id', 'name']) + ->paginate($perPage); + + return $this->fractal->collection($roles) + ->transformWith(AdminRoleTransformer::class) + ->toArray(); + } + + /** + * Returns a single role. + */ + public function view(GetRoleRequest $request, AdminRole $role): array + { + return $this->fractal->item($role) + ->transformWith(AdminRoleTransformer::class) + ->toArray(); + } + + /** + * Creates a new role. + */ + public function store(StoreRoleRequest $request): JsonResponse + { + $data = array_merge($request->validated(), [ + 'sort_id' => 99, + ]); + $role = AdminRole::query()->create($data); + + return $this->fractal->item($role) + ->transformWith(AdminRoleTransformer::class) + ->respond(JsonResponse::HTTP_CREATED); + } + + /** + * Updates a role. + */ + public function update(UpdateRoleRequest $request, AdminRole $role): array + { + $role->update($request->validated()); + + return $this->fractal->item($role) + ->transformWith(AdminRoleTransformer::class) + ->toArray(); + } + + /** + * Deletes a role. + * + * @throws \Exception + */ + public function delete(DeleteRoleRequest $request, AdminRole $role): Response + { + $role->delete(); + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index 1717c5dd8..21f0da45f 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -34,7 +34,7 @@ class DatabaseController extends ApplicationApiController public function index(GetServerDatabasesRequest $request, Server $server): array { return $this->fractal->collection($server->databases) - ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->transformWith(ServerDatabaseTransformer::class) ->toArray(); } @@ -44,7 +44,7 @@ class DatabaseController extends ApplicationApiController public function view(GetServerDatabaseRequest $request, Server $server, Database $database): array { return $this->fractal->item($database) - ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->transformWith(ServerDatabaseTransformer::class) ->toArray(); } @@ -53,11 +53,11 @@ class DatabaseController extends ApplicationApiController * * @throws \Throwable */ - public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): JsonResponse + public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response { $this->databasePasswordService->handle($database); - return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + return $this->returnNoContent(); } /** @@ -72,23 +72,19 @@ class DatabaseController extends ApplicationApiController ])); return $this->fractal->item($database) - ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) - ->addMeta([ - 'resource' => route('api.application.servers.databases.view', [ - 'server' => $server->id, - 'database' => $database->id, - ]), - ]) + ->transformWith(ServerDatabaseTransformer::class) ->respond(Response::HTTP_CREATED); } /** * Handle a request to delete a specific server database from the Panel. + * + * @throws \Exception */ - public function delete(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response + public function delete(ServerDatabaseWriteRequest $request, Database $database): Response { $this->databaseManagementService->delete($database); - return response('', 204); + return $this->returnNoContent(); } } diff --git a/app/Http/Controllers/Api/Application/Servers/ExternalServerController.php b/app/Http/Controllers/Api/Application/Servers/ExternalServerController.php index 869472f72..3f6a3624c 100644 --- a/app/Http/Controllers/Api/Application/Servers/ExternalServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ExternalServerController.php @@ -17,7 +17,7 @@ class ExternalServerController extends ApplicationApiController $server = Server::query()->where('external_id', $external_id)->firstOrFail(); return $this->fractal->item($server) - ->transformWith($this->getTransformer(ServerTransformer::class)) + ->transformWith(ServerTransformer::class) ->toArray(); } } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index 2eb69162d..ef633d46b 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -8,12 +8,16 @@ use Illuminate\Http\JsonResponse; use Spatie\QueryBuilder\QueryBuilder; use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\ServerDeletionService; +use Pterodactyl\Services\Servers\BuildModificationService; +use Pterodactyl\Services\Servers\DetailsModificationService; use Pterodactyl\Transformers\Api\Application\ServerTransformer; +use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException; use Pterodactyl\Http\Requests\Api\Application\Servers\GetServerRequest; use Pterodactyl\Http\Requests\Api\Application\Servers\GetServersRequest; use Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest; use Pterodactyl\Http\Requests\Api\Application\Servers\StoreServerRequest; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; +use Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerRequest; class ServerController extends ApplicationApiController { @@ -21,6 +25,8 @@ class ServerController extends ApplicationApiController * ServerController constructor. */ public function __construct( + private BuildModificationService $buildModificationService, + private DetailsModificationService $detailsModificationService, private ServerCreationService $creationService, private ServerDeletionService $deletionService ) { @@ -32,13 +38,18 @@ class ServerController extends ApplicationApiController */ public function index(GetServersRequest $request): array { + $perPage = (int) $request->query('per_page', '10'); + if ($perPage < 1 || $perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + $servers = QueryBuilder::for(Server::query()) - ->allowedFilters(['uuid', 'uuidShort', 'name', 'description', 'image', 'external_id']) - ->allowedSorts(['id', 'uuid']) - ->paginate($request->query('per_page') ?? 50); + ->allowedFilters(['id', 'uuid', 'uuidShort', 'name', 'owner_id', 'node_id', 'external_id']) + ->allowedSorts(['id', 'uuid', 'uuidShort', 'name', 'owner_id', 'node_id', 'status']) + ->paginate($perPage); return $this->fractal->collection($servers) - ->transformWith($this->getTransformer(ServerTransformer::class)) + ->transformWith(ServerTransformer::class) ->toArray(); } @@ -48,18 +59,17 @@ class ServerController extends ApplicationApiController * @throws \Throwable * @throws \Illuminate\Validation\ValidationException * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException */ public function store(StoreServerRequest $request): JsonResponse { - $server = $this->creationService->handle($request->validated(), $request->getDeploymentObject()); + $server = $this->creationService->handle($request->validated()); return $this->fractal->item($server) - ->transformWith($this->getTransformer(ServerTransformer::class)) - ->respond(201); + ->transformWith(ServerTransformer::class) + ->respond(Response::HTTP_CREATED); } /** @@ -68,7 +78,7 @@ class ServerController extends ApplicationApiController public function view(GetServerRequest $request, Server $server): array { return $this->fractal->item($server) - ->transformWith($this->getTransformer(ServerTransformer::class)) + ->transformWith(ServerTransformer::class) ->toArray(); } @@ -76,6 +86,7 @@ class ServerController extends ApplicationApiController * Deletes a server. * * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Throwable */ public function delete(ServerWriteRequest $request, Server $server, string $force = ''): Response { @@ -83,4 +94,24 @@ class ServerController extends ApplicationApiController return $this->returnNoContent(); } + + /** + * Update a server. + * + * @throws \Throwable + * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException + */ + public function update(UpdateServerRequest $request, Server $server): array + { + $server = $this->buildModificationService->handle($server, $request->validated()); + $server = $this->detailsModificationService->returnUpdatedModel()->handle($server, $request->validated()); + + return $this->fractal->item($server) + ->transformWith(ServerTransformer::class) + ->toArray(); + } } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php index ae5f5438c..3201c7b7d 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php @@ -25,35 +25,31 @@ class ServerDetailsController extends ApplicationApiController /** * Update the details for a specific server. * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ public function details(UpdateServerDetailsRequest $request, Server $server): array { - $updated = $this->detailsModificationService->returnUpdatedModel()->handle( + $server = $this->detailsModificationService->returnUpdatedModel()->handle( $server, $request->validated() ); - return $this->fractal->item($updated) - ->transformWith($this->getTransformer(ServerTransformer::class)) + return $this->fractal->item($server) + ->transformWith(ServerTransformer::class) ->toArray(); } /** * Update the build details for a specific server. * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ public function build(UpdateServerBuildConfigurationRequest $request, Server $server): array { $server = $this->buildModificationService->handle($server, $request->validated()); return $this->fractal->item($server) - ->transformWith($this->getTransformer(ServerTransformer::class)) + ->transformWith(ServerTransformer::class) ->toArray(); } } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php index d4dcaa24c..8e20d098b 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php @@ -12,7 +12,7 @@ use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; class ServerManagementController extends ApplicationApiController { /** - * ServerManagementController constructor. + * SuspensionController constructor. */ public function __construct( private ReinstallServerService $reinstallServerService, @@ -48,9 +48,7 @@ class ServerManagementController extends ApplicationApiController /** * Mark a server as needing to be reinstalled. * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ public function reinstall(ServerWriteRequest $request, Server $server): Response { diff --git a/app/Http/Controllers/Api/Application/Servers/StartupController.php b/app/Http/Controllers/Api/Application/Servers/StartupController.php index 6c2da078a..ea6c28ff0 100644 --- a/app/Http/Controllers/Api/Application/Servers/StartupController.php +++ b/app/Http/Controllers/Api/Application/Servers/StartupController.php @@ -22,10 +22,7 @@ class StartupController extends ApplicationApiController /** * Update the startup and environment settings for a specific server. * - * @throws \Illuminate\Validation\ValidationException - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ public function index(UpdateServerStartupRequest $request, Server $server): array { @@ -34,7 +31,7 @@ class StartupController extends ApplicationApiController ->handle($server, $request->validated()); return $this->fractal->item($server) - ->transformWith($this->getTransformer(ServerTransformer::class)) + ->transformWith(ServerTransformer::class) ->toArray(); } } diff --git a/app/Http/Controllers/Api/Application/Users/ExternalUserController.php b/app/Http/Controllers/Api/Application/Users/ExternalUserController.php index 2a8f4f07e..380c0cc3b 100644 --- a/app/Http/Controllers/Api/Application/Users/ExternalUserController.php +++ b/app/Http/Controllers/Api/Application/Users/ExternalUserController.php @@ -17,7 +17,7 @@ class ExternalUserController extends ApplicationApiController $user = User::query()->where('external_id', $external_id)->firstOrFail(); return $this->fractal->item($user) - ->transformWith($this->getTransformer(UserTransformer::class)) + ->transformWith(UserTransformer::class) ->toArray(); } } diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index 0ddae27b1..48fd58133 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -2,13 +2,19 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Users; +use Illuminate\Support\Arr; use Pterodactyl\Models\User; +use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; use Spatie\QueryBuilder\QueryBuilder; +use Spatie\QueryBuilder\AllowedFilter; +use Illuminate\Database\Eloquent\Builder; use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Services\Users\UserDeletionService; use Pterodactyl\Transformers\Api\Application\UserTransformer; +use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException; +use Pterodactyl\Http\Requests\Api\Application\Users\GetUserRequest; use Pterodactyl\Http\Requests\Api\Application\Users\GetUsersRequest; use Pterodactyl\Http\Requests\Api\Application\Users\StoreUserRequest; use Pterodactyl\Http\Requests\Api\Application\Users\DeleteUserRequest; @@ -35,24 +41,48 @@ class UserController extends ApplicationApiController */ public function index(GetUsersRequest $request): array { + $perPage = (int) $request->query('per_page', '10'); + if ($perPage < 1 || $perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + $users = QueryBuilder::for(User::query()) - ->allowedFilters(['email', 'uuid', 'username', 'external_id']) - ->allowedSorts(['id', 'uuid']) - ->paginate($request->query('per_page') ?? 50); + ->allowedFilters([ + AllowedFilter::exact('id'), + AllowedFilter::exact('uuid'), + AllowedFilter::exact('external_id'), + 'username', + 'email', + AllowedFilter::callback('*', function (Builder $builder, $value) { + foreach (Arr::wrap($value) as $datum) { + $datum = '%' . $datum . '%'; + $builder->where(function (Builder $builder) use ($datum) { + $builder->where('uuid', 'LIKE', $datum) + ->orWhere('username', 'LIKE', $datum) + ->orWhere('email', 'LIKE', $datum) + ->orWhere('external_id', 'LIKE', $datum); + }); + } + }), + ]) + ->allowedSorts(['id', 'uuid', 'username', 'email', 'admin_role_id']) + ->paginate($perPage); return $this->fractal->collection($users) - ->transformWith($this->getTransformer(UserTransformer::class)) + ->transformWith(UserTransformer::class) ->toArray(); } /** * Handle a request to view a single user. Includes any relations that * were defined in the request. + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - public function view(GetUsersRequest $request, User $user): array + public function view(GetUserRequest $request, User $user): array { return $this->fractal->item($user) - ->transformWith($this->getTransformer(UserTransformer::class)) + ->transformWith(UserTransformer::class) ->toArray(); } @@ -64,22 +94,20 @@ class UserController extends ApplicationApiController * Revocation errors are returned under the 'revocation_errors' key in the response * meta. If there are no errors this is an empty array. * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function update(UpdateUserRequest $request, User $user): array { $this->updateService->setUserLevel(User::USER_LEVEL_ADMIN); $user = $this->updateService->handle($user, $request->validated()); - $response = $this->fractal->item($user) - ->transformWith($this->getTransformer(UserTransformer::class)); - - return $response->toArray(); + return $this->fractal->item($user) + ->transformWith(UserTransformer::class) + ->toArray(); } /** - * Store a new user on the system. Returns the created user and an HTTP/201 + * Store a new user on the system. Returns the created user and a HTTP/201 * header on successful creation. * * @throws \Exception @@ -90,12 +118,7 @@ class UserController extends ApplicationApiController $user = $this->creationService->handle($request->validated()); return $this->fractal->item($user) - ->transformWith($this->getTransformer(UserTransformer::class)) - ->addMeta([ - 'resource' => route('api.application.users.view', [ - 'user' => $user->id, - ]), - ]) + ->transformWith(UserTransformer::class) ->respond(201); } @@ -105,10 +128,10 @@ class UserController extends ApplicationApiController * * @throws \Pterodactyl\Exceptions\DisplayException */ - public function delete(DeleteUserRequest $request, User $user): JsonResponse + public function delete(DeleteUserRequest $request, User $user): Response { $this->deletionService->handle($user); - return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + return $this->returnNoContent(); } } diff --git a/app/Http/Controllers/Api/Application/VersionController.php b/app/Http/Controllers/Api/Application/VersionController.php new file mode 100644 index 000000000..9bd2203ed --- /dev/null +++ b/app/Http/Controllers/Api/Application/VersionController.php @@ -0,0 +1,25 @@ +softwareVersionService->getVersionData()); + } +} diff --git a/app/Http/Controllers/Api/Client/AccountController.php b/app/Http/Controllers/Api/Client/AccountController.php index cbe8b0315..468a751c0 100644 --- a/app/Http/Controllers/Api/Client/AccountController.php +++ b/app/Http/Controllers/Api/Client/AccountController.php @@ -25,7 +25,7 @@ class AccountController extends ClientApiController public function index(Request $request): array { return $this->fractal->item($request->user()) - ->transformWith($this->getTransformer(AccountTransformer::class)) + ->transformWith(AccountTransformer::class) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/ActivityLogController.php b/app/Http/Controllers/Api/Client/ActivityLogController.php index dbbd06c06..f6e9033e2 100644 --- a/app/Http/Controllers/Api/Client/ActivityLogController.php +++ b/app/Http/Controllers/Api/Client/ActivityLogController.php @@ -24,7 +24,7 @@ class ActivityLogController extends ClientApiController ->appends($request->query()); return $this->fractal->collection($activity) - ->transformWith($this->getTransformer(ActivityLogTransformer::class)) + ->transformWith(ActivityLogTransformer::class) ->toArray(); } } diff --git a/app/Http/Controllers/Api/Client/ApiKeyController.php b/app/Http/Controllers/Api/Client/ApiKeyController.php index ac00a4d8f..e60ac59cd 100644 --- a/app/Http/Controllers/Api/Client/ApiKeyController.php +++ b/app/Http/Controllers/Api/Client/ApiKeyController.php @@ -18,7 +18,7 @@ class ApiKeyController extends ClientApiController public function index(ClientApiRequest $request): array { return $this->fractal->collection($request->user()->apiKeys) - ->transformWith($this->getTransformer(ApiKeyTransformer::class)) + ->transformWith(ApiKeyTransformer::class) ->toArray(); } @@ -44,7 +44,7 @@ class ApiKeyController extends ClientApiController ->log(); return $this->fractal->item($token->accessToken) - ->transformWith($this->getTransformer(ApiKeyTransformer::class)) + ->transformWith(ApiKeyTransformer::class) ->addMeta(['secret_token' => $token->plainTextToken]) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/ClientApiController.php b/app/Http/Controllers/Api/Client/ClientApiController.php index a468d762a..380cbf548 100644 --- a/app/Http/Controllers/Api/Client/ClientApiController.php +++ b/app/Http/Controllers/Api/Client/ClientApiController.php @@ -3,7 +3,7 @@ namespace Pterodactyl\Http\Controllers\Api\Client; use Webmozart\Assert\Assert; -use Pterodactyl\Transformers\Api\Client\BaseClientTransformer; +use Pterodactyl\Transformers\Api\Transformer; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; abstract class ClientApiController extends ApplicationApiController @@ -11,7 +11,7 @@ abstract class ClientApiController extends ApplicationApiController /** * Returns only the includes which are valid for the given transformer. */ - protected function getIncludesForTransformer(BaseClientTransformer $transformer, array $merge = []): array + protected function getIncludesForTransformer(Transformer $transformer, array $merge = []): array { $filtered = array_filter($this->parseIncludes(), function ($datum) use ($transformer) { return in_array($datum, $transformer->getAvailableIncludes()); @@ -35,22 +35,4 @@ abstract class ClientApiController extends ApplicationApiController return trim($item); }, explode(',', $includes)); } - - /** - * Return an instance of an application transformer. - * - * @template T of \Pterodactyl\Transformers\Api\Client\BaseClientTransformer - * - * @param class-string $abstract - * - * @return T - * - * @noinspection PhpDocSignatureInspection - */ - public function getTransformer(string $abstract) - { - Assert::subclassOf($abstract, BaseClientTransformer::class); - - return $abstract::fromRequest($this->request); - } } diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index dcdb5964b..d4d5ae6c5 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -27,7 +27,7 @@ class ClientController extends ClientApiController public function index(GetServersRequest $request): array { $user = $request->user(); - $transformer = $this->getTransformer(ServerTransformer::class); + $transformer = new ServerTransformer(); // Start the query builder and ensure we eager load any requested relationships from the request. $builder = QueryBuilder::for( diff --git a/app/Http/Controllers/Api/Client/SSHKeyController.php b/app/Http/Controllers/Api/Client/SSHKeyController.php index aa0f0c686..794c44cf0 100644 --- a/app/Http/Controllers/Api/Client/SSHKeyController.php +++ b/app/Http/Controllers/Api/Client/SSHKeyController.php @@ -17,7 +17,7 @@ class SSHKeyController extends ClientApiController public function index(ClientApiRequest $request): array { return $this->fractal->collection($request->user()->sshKeys) - ->transformWith($this->getTransformer(UserSSHKeyTransformer::class)) + ->transformWith(UserSSHKeyTransformer::class) ->toArray(); } @@ -38,7 +38,7 @@ class SSHKeyController extends ClientApiController ->log(); return $this->fractal->item($model) - ->transformWith($this->getTransformer(UserSSHKeyTransformer::class)) + ->transformWith(UserSSHKeyTransformer::class) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php b/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php index f569c4122..e3e083d48 100644 --- a/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php +++ b/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php @@ -48,7 +48,7 @@ class ActivityLogController extends ClientApiController ->appends($request->query()); return $this->fractal->collection($activity) - ->transformWith($this->getTransformer(ActivityLogTransformer::class)) + ->transformWith(ActivityLogTransformer::class) ->toArray(); } } diff --git a/app/Http/Controllers/Api/Client/Servers/BackupController.php b/app/Http/Controllers/Api/Client/Servers/BackupController.php index 7a35341c1..13c9ac7ea 100644 --- a/app/Http/Controllers/Api/Client/Servers/BackupController.php +++ b/app/Http/Controllers/Api/Client/Servers/BackupController.php @@ -49,7 +49,7 @@ class BackupController extends ClientApiController $limit = min($request->query('per_page') ?? 20, 50); return $this->fractal->collection($server->backups()->paginate($limit)) - ->transformWith($this->getTransformer(BackupTransformer::class)) + ->transformWith(BackupTransformer::class) ->addMeta([ 'backup_count' => $this->repository->getNonFailedBackups($server)->count(), ]) @@ -84,7 +84,7 @@ class BackupController extends ClientApiController ->log(); return $this->fractal->item($backup) - ->transformWith($this->getTransformer(BackupTransformer::class)) + ->transformWith(BackupTransformer::class) ->toArray(); } @@ -107,7 +107,7 @@ class BackupController extends ClientApiController Activity::event($action)->subject($backup)->property('name', $backup->name)->log(); return $this->fractal->item($backup) - ->transformWith($this->getTransformer(BackupTransformer::class)) + ->transformWith(BackupTransformer::class) ->toArray(); } @@ -123,7 +123,7 @@ class BackupController extends ClientApiController } return $this->fractal->item($backup) - ->transformWith($this->getTransformer(BackupTransformer::class)) + ->transformWith(BackupTransformer::class) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php index 728c67e91..5173cab2c 100644 --- a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php @@ -35,7 +35,7 @@ class DatabaseController extends ClientApiController public function index(GetDatabasesRequest $request, Server $server): array { return $this->fractal->collection($server->databases) - ->transformWith($this->getTransformer(DatabaseTransformer::class)) + ->transformWith(DatabaseTransformer::class) ->toArray(); } @@ -57,7 +57,7 @@ class DatabaseController extends ClientApiController return $this->fractal->item($database) ->parseIncludes(['password']) - ->transformWith($this->getTransformer(DatabaseTransformer::class)) + ->transformWith(DatabaseTransformer::class) ->toArray(); } @@ -79,7 +79,7 @@ class DatabaseController extends ClientApiController return $this->fractal->item($database) ->parseIncludes(['password']) - ->transformWith($this->getTransformer(DatabaseTransformer::class)) + ->transformWith(DatabaseTransformer::class) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index 23e6718a5..eaee7f8cc 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -47,7 +47,7 @@ class FileController extends ClientApiController ->getDirectory($request->get('directory') ?? '/'); return $this->fractal->collection($contents) - ->transformWith($this->getTransformer(FileObjectTransformer::class)) + ->transformWith(FileObjectTransformer::class) ->toArray(); } @@ -183,7 +183,7 @@ class FileController extends ClientApiController ->log(); return $this->fractal->item($file) - ->transformWith($this->getTransformer(FileObjectTransformer::class)) + ->transformWith(FileObjectTransformer::class) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php index 4e3c5f9bb..2fd97101b 100644 --- a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php +++ b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php @@ -36,7 +36,7 @@ class NetworkAllocationController extends ClientApiController public function index(GetNetworkRequest $request, Server $server): array { return $this->fractal->collection($server->allocations) - ->transformWith($this->getTransformer(AllocationTransformer::class)) + ->transformWith(AllocationTransformer::class) ->toArray(); } @@ -60,7 +60,7 @@ class NetworkAllocationController extends ClientApiController } return $this->fractal->item($allocation) - ->transformWith($this->getTransformer(AllocationTransformer::class)) + ->transformWith(AllocationTransformer::class) ->toArray(); } @@ -80,7 +80,7 @@ class NetworkAllocationController extends ClientApiController ->log(); return $this->fractal->item($allocation) - ->transformWith($this->getTransformer(AllocationTransformer::class)) + ->transformWith(AllocationTransformer::class) ->toArray(); } @@ -104,7 +104,7 @@ class NetworkAllocationController extends ClientApiController ->log(); return $this->fractal->item($allocation) - ->transformWith($this->getTransformer(AllocationTransformer::class)) + ->transformWith(AllocationTransformer::class) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/Servers/ResourceUtilizationController.php b/app/Http/Controllers/Api/Client/Servers/ResourceUtilizationController.php index dcaf48115..daf3219a1 100644 --- a/app/Http/Controllers/Api/Client/Servers/ResourceUtilizationController.php +++ b/app/Http/Controllers/Api/Client/Servers/ResourceUtilizationController.php @@ -35,7 +35,7 @@ class ResourceUtilizationController extends ClientApiController }); return $this->fractal->item($stats) - ->transformWith($this->getTransformer(StatsTransformer::class)) + ->transformWith(StatsTransformer::class) ->toArray(); } } diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php index b955fb16b..20fc4e658 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php @@ -40,7 +40,7 @@ class ScheduleController extends ClientApiController $schedules = $server->schedules->loadMissing('tasks'); return $this->fractal->collection($schedules) - ->transformWith($this->getTransformer(ScheduleTransformer::class)) + ->transformWith(ScheduleTransformer::class) ->toArray(); } @@ -72,7 +72,7 @@ class ScheduleController extends ClientApiController ->log(); return $this->fractal->item($model) - ->transformWith($this->getTransformer(ScheduleTransformer::class)) + ->transformWith(ScheduleTransformer::class) ->toArray(); } @@ -88,7 +88,7 @@ class ScheduleController extends ClientApiController $schedule->loadMissing('tasks'); return $this->fractal->item($schedule) - ->transformWith($this->getTransformer(ScheduleTransformer::class)) + ->transformWith(ScheduleTransformer::class) ->toArray(); } @@ -131,7 +131,7 @@ class ScheduleController extends ClientApiController ->log(); return $this->fractal->item($schedule->refresh()) - ->transformWith($this->getTransformer(ScheduleTransformer::class)) + ->transformWith(ScheduleTransformer::class) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php index addca2876..e6427753b 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php @@ -64,7 +64,7 @@ class ScheduleTaskController extends ClientApiController ->log(); return $this->fractal->item($task) - ->transformWith($this->getTransformer(TaskTransformer::class)) + ->transformWith(TaskTransformer::class) ->toArray(); } @@ -97,7 +97,7 @@ class ScheduleTaskController extends ClientApiController ->log(); return $this->fractal->item($task->refresh()) - ->transformWith($this->getTransformer(TaskTransformer::class)) + ->transformWith(TaskTransformer::class) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/Servers/ServerController.php b/app/Http/Controllers/Api/Client/Servers/ServerController.php index 63eb9b988..ac898e2b6 100644 --- a/app/Http/Controllers/Api/Client/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Client/Servers/ServerController.php @@ -25,7 +25,7 @@ class ServerController extends ClientApiController public function index(GetServerRequest $request, Server $server): array { return $this->fractal->item($server) - ->transformWith($this->getTransformer(ServerTransformer::class)) + ->transformWith(ServerTransformer::class) ->addMeta([ 'is_server_owner' => $request->user()->id === $server->owner_id, 'user_permissions' => $this->permissionsService->handle($server, $request->user()), diff --git a/app/Http/Controllers/Api/Client/Servers/StartupController.php b/app/Http/Controllers/Api/Client/Servers/StartupController.php index b674145ff..56b632617 100644 --- a/app/Http/Controllers/Api/Client/Servers/StartupController.php +++ b/app/Http/Controllers/Api/Client/Servers/StartupController.php @@ -34,7 +34,7 @@ class StartupController extends ClientApiController return $this->fractal->collection( $server->variables()->where('user_viewable', true)->get() ) - ->transformWith($this->getTransformer(EggVariableTransformer::class)) + ->transformWith(EggVariableTransformer::class) ->addMeta([ 'startup_command' => $startup, 'docker_images' => $server->egg->docker_images, @@ -90,7 +90,7 @@ class StartupController extends ClientApiController } return $this->fractal->item($variable) - ->transformWith($this->getTransformer(EggVariableTransformer::class)) + ->transformWith(EggVariableTransformer::class) ->addMeta([ 'startup_command' => $startup, 'raw_startup_command' => $server->startup, diff --git a/app/Http/Controllers/Api/Client/Servers/SubuserController.php b/app/Http/Controllers/Api/Client/Servers/SubuserController.php index 2c403c691..ac402abb9 100644 --- a/app/Http/Controllers/Api/Client/Servers/SubuserController.php +++ b/app/Http/Controllers/Api/Client/Servers/SubuserController.php @@ -38,7 +38,7 @@ class SubuserController extends ClientApiController public function index(GetSubuserRequest $request, Server $server): array { return $this->fractal->collection($server->subusers) - ->transformWith($this->getTransformer(SubuserTransformer::class)) + ->transformWith(SubuserTransformer::class) ->toArray(); } @@ -50,7 +50,7 @@ class SubuserController extends ClientApiController $subuser = $request->attributes->get('subuser'); return $this->fractal->item($subuser) - ->transformWith($this->getTransformer(SubuserTransformer::class)) + ->transformWith(SubuserTransformer::class) ->toArray(); } @@ -76,7 +76,7 @@ class SubuserController extends ClientApiController ->log(); return $this->fractal->item($response) - ->transformWith($this->getTransformer(SubuserTransformer::class)) + ->transformWith(SubuserTransformer::class) ->toArray(); } @@ -129,7 +129,7 @@ class SubuserController extends ClientApiController $log->reset(); return $this->fractal->item($subuser->refresh()) - ->transformWith($this->getTransformer(SubuserTransformer::class)) + ->transformWith(SubuserTransformer::class) ->toArray(); } diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php index 74e7a9aa3..c76a5afc2 100644 --- a/app/Http/Controllers/Auth/AbstractLoginController.php +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -85,7 +85,7 @@ abstract class AbstractLoginController extends Controller 'data' => [ 'complete' => true, 'intended' => $this->redirectPath(), - 'user' => $user->toVueObject(), + 'user' => $user->toReactObject(), ], ]); } diff --git a/app/Http/Requests/Api/ApiRequest.php b/app/Http/Requests/Api/ApiRequest.php new file mode 100644 index 000000000..253a61691 --- /dev/null +++ b/app/Http/Requests/Api/ApiRequest.php @@ -0,0 +1,83 @@ +passesAuthorization()) { + $this->failedAuthorization(); + } + + $this->hasValidated = true; + } + + /* + * Determine if the request passes the authorization check as well + * as the exists check. + * + * @return bool + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + protected function passesAuthorization() + { + // If we have already validated we do not need to call this function + // again. This is needed to work around Laravel's normal auth validation + // that occurs after validating the request params since we are doing auth + // validation in the prepareForValidation() function. + if ($this->hasValidated) { + return true; + } + + if (!parent::passesAuthorization()) { + return false; + } + + // Only let the user know that a resource does not exist if they are + // authenticated to access the endpoint. This avoids exposing that + // an item exists (or does not exist) to the user until they can prove + // that they have permission to know about it. + if ($this->attributes->get('is_missing_model', false)) { + throw new NotFoundHttpException(trans('exceptions.api.resource_not_found')); + } + + return true; + } +} diff --git a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php b/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php index 6529a9a5a..d062f7648 100644 --- a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php +++ b/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Allocations; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class DeleteAllocationRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS; - - protected int $permission = AdminAcl::WRITE; } diff --git a/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php b/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php index f03223f2d..2f536af60 100644 --- a/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php +++ b/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Allocations; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetAllocationsRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS; - - protected int $permission = AdminAcl::READ; } diff --git a/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php b/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php index a7e0c4da2..0474408ab 100644 --- a/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php +++ b/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php @@ -2,15 +2,11 @@ namespace Pterodactyl\Http\Requests\Api\Application\Allocations; -use Pterodactyl\Services\Acl\Api\AdminAcl; +use Illuminate\Support\Arr; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class StoreAllocationRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS; - - protected int $permission = AdminAcl::WRITE; - public function rules(): array { return [ @@ -21,14 +17,22 @@ class StoreAllocationRequest extends ApplicationApiRequest ]; } - public function validated($key = null, $default = null): array + /** + * @param string|null $key + * @param string|array|null $default + * + * @return mixed + */ + public function validated($key = null, $default = null) { $data = parent::validated(); - return [ + $response = [ 'allocation_ip' => $data['ip'], 'allocation_ports' => $data['ports'], 'allocation_alias' => $data['alias'] ?? null, ]; + + return is_null($key) ? $response : Arr::get($response, $key, $default); } } diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index 082bc6921..70f6cb219 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -2,94 +2,16 @@ namespace Pterodactyl\Http\Requests\Api\Application; -use Webmozart\Assert\Assert; -use Pterodactyl\Models\ApiKey; -use Laravel\Sanctum\TransientToken; -use Illuminate\Validation\Validator; -use Illuminate\Database\Eloquent\Model; -use Pterodactyl\Services\Acl\Api\AdminAcl; -use Illuminate\Foundation\Http\FormRequest; -use Pterodactyl\Exceptions\PterodactylException; +use Pterodactyl\Http\Requests\Api\ApiRequest; -abstract class ApplicationApiRequest extends FormRequest +abstract class ApplicationApiRequest extends ApiRequest { /** - * The resource that should be checked when performing the authorization - * function for this request. - */ - protected ?string $resource; - - /** - * The permission level that a given API key should have for accessing - * the defined $resource during the request cycle. - */ - protected int $permission = AdminAcl::NONE; - - /** - * Determine if the current user is authorized to perform - * the requested action against the API. - * - * @throws PterodactylException + * This will eventually be replaced with per-request permissions checking + * on the API key and for the user. */ public function authorize(): bool { - if (is_null($this->resource)) { - throw new PterodactylException('An ACL resource must be defined on API requests.'); - } - - $token = $this->user()->currentAccessToken(); - if ($token instanceof TransientToken) { - return true; - } - - /** @var ApiKey $token */ - if ($token->key_type === ApiKey::TYPE_ACCOUNT) { - return true; - } - - return AdminAcl::check($token, $this->resource, $this->permission); - } - - /** - * Default set of rules to apply to API requests. - */ - public function rules(): array - { - return []; - } - - /** - * Helper method allowing a developer to easily hook into this logic without having - * to remember what the method name is called or where to use it. By default this is - * a no-op. - */ - public function withValidator(Validator $validator): void - { - // do nothing - } - - /** - * Returns the named route parameter and asserts that it is a real model that - * exists in the database. - * - * @template T of \Illuminate\Database\Eloquent\Model - * - * @param class-string $expect - * - * @return T - * - * @noinspection PhpDocSignatureInspection - */ - public function parameter(string $key, string $expect) - { - /** @var ApiKey $value */ - $value = $this->route()->parameter($key); - - Assert::isInstanceOf($value, $expect); - Assert::isInstanceOf($value, Model::class); - Assert::true($value->exists); - - /* @var T $value */ - return $value; + return $this->user()->root_admin; } } diff --git a/app/Http/Requests/Api/Application/Databases/DeleteDatabaseRequest.php b/app/Http/Requests/Api/Application/Databases/DeleteDatabaseRequest.php new file mode 100644 index 000000000..cde56da1c --- /dev/null +++ b/app/Http/Requests/Api/Application/Databases/DeleteDatabaseRequest.php @@ -0,0 +1,9 @@ +route()->parameter('databaseHost')->id); + } +} diff --git a/app/Http/Requests/Api/Application/Eggs/DeleteEggRequest.php b/app/Http/Requests/Api/Application/Eggs/DeleteEggRequest.php new file mode 100644 index 000000000..154f06efd --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/DeleteEggRequest.php @@ -0,0 +1,16 @@ +route()->parameter('egg'); + + return $egg instanceof Egg && $egg->exists; + } +} diff --git a/app/Http/Requests/Api/Application/Eggs/ExportEggRequest.php b/app/Http/Requests/Api/Application/Eggs/ExportEggRequest.php new file mode 100644 index 000000000..63893df54 --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/ExportEggRequest.php @@ -0,0 +1,9 @@ + 'required|bail|numeric|exists:nests,id', + 'name' => 'required|string|max:191', + 'description' => 'sometimes|string|nullable', + 'features' => 'sometimes|array', + 'docker_images' => 'required|array|min:1', + 'docker_images.*' => 'required|string', + 'file_denylist' => 'sometimes|array|nullable', + 'file_denylist.*' => 'sometimes|string', + 'config_files' => 'required|nullable|json', + 'config_startup' => 'required|nullable|json', + 'config_stop' => 'required|nullable|string|max:191', +// 'config_from' => 'sometimes|nullable|numeric|exists:eggs,id', + 'startup' => 'required|string', + 'script_container' => 'sometimes|string', + 'script_entry' => 'sometimes|string', + 'script_install' => 'sometimes|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Eggs/UpdateEggRequest.php b/app/Http/Requests/Api/Application/Eggs/UpdateEggRequest.php new file mode 100644 index 000000000..090f5b51c --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/UpdateEggRequest.php @@ -0,0 +1,28 @@ + 'sometimes|numeric|exists:nests,id', + 'name' => 'sometimes|string|max:191', + 'description' => 'sometimes|string|nullable', + 'features' => 'sometimes|array', + 'docker_images' => 'sometimes|array|min:1', + 'docker_images.*' => 'sometimes|string', + 'file_denylist' => 'sometimes|array|nullable', + 'file_denylist.*' => 'sometimes|string', + 'config_files' => 'sometimes|nullable|json', + 'config_startup' => 'sometimes|nullable|json', + 'config_stop' => 'sometimes|nullable|string|max:191', +// 'config_from' => 'sometimes|nullable|numeric|exists:eggs,id', + 'startup' => 'sometimes|string', + 'script_container' => 'sometimes|string', + 'script_entry' => 'sometimes|string', + 'script_install' => 'sometimes|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Eggs/Variables/StoreEggVariableRequest.php b/app/Http/Requests/Api/Application/Eggs/Variables/StoreEggVariableRequest.php new file mode 100644 index 000000000..9c674a9d8 --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/Variables/StoreEggVariableRequest.php @@ -0,0 +1,22 @@ + 'required|string|min:1|max:191', + 'description' => 'sometimes|string|nullable', + 'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES, + 'default_value' => 'present', + 'user_viewable' => 'required|boolean', + 'user_editable' => 'required|boolean', + 'rules' => 'bail|required|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Eggs/Variables/UpdateEggVariablesRequest.php b/app/Http/Requests/Api/Application/Eggs/Variables/UpdateEggVariablesRequest.php new file mode 100644 index 000000000..c15de2ce3 --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/Variables/UpdateEggVariablesRequest.php @@ -0,0 +1,24 @@ + 'array', + '*.id' => 'required|integer', + '*.name' => 'sometimes|string|min:1|max:191', + '*.description' => 'sometimes|string|nullable', + '*.env_variable' => 'sometimes|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES, + '*.default_value' => 'sometimes|present', + '*.user_viewable' => 'sometimes|boolean', + '*.user_editable' => 'sometimes|boolean', + '*.rules' => 'sometimes|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php index 3c41e1166..eb2cffd34 100644 --- a/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Locations; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class DeleteLocationRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_LOCATIONS; - - protected int $permission = AdminAcl::WRITE; } diff --git a/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php b/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php index 65157dc47..dea300b91 100644 --- a/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php +++ b/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Locations; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetLocationsRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_LOCATIONS; - - protected int $permission = AdminAcl::READ; } diff --git a/app/Http/Requests/Api/Application/Locations/StoreLocationRequest.php b/app/Http/Requests/Api/Application/Locations/StoreLocationRequest.php index cf0b12629..9b403fa10 100644 --- a/app/Http/Requests/Api/Application/Locations/StoreLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/StoreLocationRequest.php @@ -3,18 +3,10 @@ namespace Pterodactyl\Http\Requests\Api\Application\Locations; use Pterodactyl\Models\Location; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class StoreLocationRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_LOCATIONS; - - protected int $permission = AdminAcl::WRITE; - - /** - * Rules to validate the request against. - */ public function rules(): array { return collect(Location::getRules())->only([ @@ -23,9 +15,6 @@ class StoreLocationRequest extends ApplicationApiRequest ])->toArray(); } - /** - * Rename fields to be more clear in error messages. - */ public function attributes(): array { return [ diff --git a/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php b/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php index b7acac978..91ece11fe 100644 --- a/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php @@ -6,14 +6,9 @@ use Pterodactyl\Models\Location; class UpdateLocationRequest extends StoreLocationRequest { - /** - * Rules to validate this request against. - */ public function rules(): array { - /** @var Location $location */ - $location = $this->route()->parameter('location'); - $locationId = $location->id; + $locationId = $this->route()->parameter('location')->id; return collect(Location::getRulesForUpdate($locationId))->only([ 'short', diff --git a/app/Http/Requests/Api/Application/Mounts/DeleteMountRequest.php b/app/Http/Requests/Api/Application/Mounts/DeleteMountRequest.php new file mode 100644 index 000000000..1325510f2 --- /dev/null +++ b/app/Http/Requests/Api/Application/Mounts/DeleteMountRequest.php @@ -0,0 +1,9 @@ + 'required|exists:eggs,id']; + } +} diff --git a/app/Http/Requests/Api/Application/Mounts/MountNodesRequest.php b/app/Http/Requests/Api/Application/Mounts/MountNodesRequest.php new file mode 100644 index 000000000..4810591a8 --- /dev/null +++ b/app/Http/Requests/Api/Application/Mounts/MountNodesRequest.php @@ -0,0 +1,13 @@ + 'required|exists:nodes,id']; + } +} diff --git a/app/Http/Requests/Api/Application/Mounts/StoreMountRequest.php b/app/Http/Requests/Api/Application/Mounts/StoreMountRequest.php new file mode 100644 index 000000000..ba678d186 --- /dev/null +++ b/app/Http/Requests/Api/Application/Mounts/StoreMountRequest.php @@ -0,0 +1,14 @@ +route()->parameter('mount')->id); + } +} diff --git a/app/Http/Requests/Api/Application/Nests/DeleteNestRequest.php b/app/Http/Requests/Api/Application/Nests/DeleteNestRequest.php new file mode 100644 index 000000000..8d505c93e --- /dev/null +++ b/app/Http/Requests/Api/Application/Nests/DeleteNestRequest.php @@ -0,0 +1,9 @@ +route()->parameter('nest')->id); + } +} diff --git a/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php index 01f503f3f..4bd5159d0 100644 --- a/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class DeleteNodeRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_NODES; - - protected int $permission = AdminAcl::WRITE; } diff --git a/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php b/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php index 5c8524b94..ac6191ea5 100644 --- a/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetNodesRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_NODES; - - protected int $permission = AdminAcl::READ; } diff --git a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php index 4fe705448..803f0e6a3 100644 --- a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php @@ -2,48 +2,50 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; +use Illuminate\Support\Arr; use Pterodactyl\Models\Node; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class StoreNodeRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_NODES; - - protected int $permission = AdminAcl::WRITE; - /** * Validation rules to apply to this request. */ public function rules(array $rules = null): array { return collect($rules ?? Node::getRules())->only([ - 'public', 'name', + 'description', 'location_id', + 'database_host_id', 'fqdn', 'scheme', 'behind_proxy', - 'maintenance_mode', + 'public', + + 'listen_port_http', + 'public_port_http', + 'listen_port_sftp', + 'public_port_sftp', + 'memory', 'memory_overallocate', 'disk', 'disk_overallocate', - 'upload_size', - 'daemonListen', - 'daemonSFTP', - 'daemonBase', - ])->mapWithKeys(function ($value, $key) { - $key = ($key === 'daemonSFTP') ? 'daemonSftp' : $key; + 'upload_size', + 'daemon_base', + ])->mapWithKeys(function ($value, $key) { return [snake_case($key) => $value]; })->toArray(); } /** * Fields to rename for clarity in the API response. + * + * @return array */ - public function attributes(): array + public function attributes() { return [ 'daemon_base' => 'Daemon Base Path', @@ -56,15 +58,20 @@ class StoreNodeRequest extends ApplicationApiRequest /** * Change the formatting of some data keys in the validated response data * to match what the application expects in the services. + * + * @param string|null $key + * @param string|array|null $default + * + * @return mixed */ - public function validated($key = null, $default = null): array + public function validated($key = null, $default = null) { $response = parent::validated(); - $response['daemonListen'] = $response['daemon_listen']; - $response['daemonSFTP'] = $response['daemon_sftp']; - $response['daemonBase'] = $response['daemon_base'] ?? (new Node())->getAttribute('daemonBase'); + $response['daemon_base'] = $response['daemon_base'] ?? Node::DEFAULT_DAEMON_BASE; - unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']); + if (!is_null($key)) { + return Arr::get($response, $key, $default); + } return $response; } diff --git a/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php index de6b7b45c..05daebc2e 100644 --- a/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php @@ -6,15 +6,8 @@ use Pterodactyl\Models\Node; class UpdateNodeRequest extends StoreNodeRequest { - /** - * Apply validation rules to this request. Uses the parent class rules() - * function but passes in the rules for updating rather than creating. - */ public function rules(array $rules = null): array { - /** @var Node $node */ - $node = $this->route()->parameter('node'); - - return parent::rules(Node::getRulesForUpdate($node->id)); + return parent::rules($rules ?? Node::getRulesForUpdate($this->route()->parameter('node')->id)); } } diff --git a/app/Http/Requests/Api/Application/Roles/DeleteRoleRequest.php b/app/Http/Requests/Api/Application/Roles/DeleteRoleRequest.php new file mode 100644 index 000000000..5f6cd34b5 --- /dev/null +++ b/app/Http/Requests/Api/Application/Roles/DeleteRoleRequest.php @@ -0,0 +1,9 @@ +route()->parameter('role')->id); + } +} diff --git a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php index 01df4af32..dd1dd2fd1 100644 --- a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php +++ b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetServerDatabaseRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_SERVER_DATABASES; - - protected int $permission = AdminAcl::READ; } diff --git a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php index ce72bbc20..74f942278 100644 --- a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php +++ b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetServerDatabasesRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_SERVER_DATABASES; - - protected int $permission = AdminAcl::READ; } diff --git a/app/Http/Requests/Api/Application/Servers/Databases/ServerDatabaseWriteRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/ServerDatabaseWriteRequest.php index 66cec82c3..827d68c55 100644 --- a/app/Http/Requests/Api/Application/Servers/Databases/ServerDatabaseWriteRequest.php +++ b/app/Http/Requests/Api/Application/Servers/Databases/ServerDatabaseWriteRequest.php @@ -2,9 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases; -use Pterodactyl\Services\Acl\Api\AdminAcl; - class ServerDatabaseWriteRequest extends GetServerDatabasesRequest { - protected int $permission = AdminAcl::WRITE; } diff --git a/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php index f52bed805..17368cd08 100644 --- a/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php +++ b/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php @@ -2,26 +2,18 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases; +use Illuminate\Support\Arr; use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; use Illuminate\Validation\Rule; use Illuminate\Database\Query\Builder; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class StoreServerDatabaseRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_SERVER_DATABASES; - - protected int $permission = AdminAcl::WRITE; - - /** - * Validation rules for database creation. - */ public function rules(): array { - /** @var Server $server */ $server = $this->route()->parameter('server'); return [ @@ -40,20 +32,22 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest } /** - * Return data formatted in the correct format for the service to consume. + * @param string|null $key + * @param string|array|null $default + * + * @return mixed */ - public function validated($key = null, $default = null): array + public function validated($key = null, $default = null) { - return [ + $data = [ 'database' => $this->input('database'), 'remote' => $this->input('remote'), 'database_host_id' => $this->input('host'), ]; + + return is_null($key) ? $data : Arr::get($data, $key, $default); } - /** - * Format error messages in a more understandable format for API output. - */ public function attributes(): array { return [ @@ -63,12 +57,8 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest ]; } - /** - * Returns the database name in the expected format. - */ public function databaseName(): string { - /** @var Server $server */ $server = $this->route()->parameter('server'); Assert::isInstanceOf($server, Server::class); diff --git a/app/Http/Requests/Api/Application/Servers/GetExternalServerRequest.php b/app/Http/Requests/Api/Application/Servers/GetExternalServerRequest.php index 50c9dabf8..790f55798 100644 --- a/app/Http/Requests/Api/Application/Servers/GetExternalServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/GetExternalServerRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetExternalServerRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_SERVERS; - - protected int $permission = AdminAcl::READ; } diff --git a/app/Http/Requests/Api/Application/Servers/GetServerRequest.php b/app/Http/Requests/Api/Application/Servers/GetServerRequest.php index 63c4ea86a..2f4f417cd 100644 --- a/app/Http/Requests/Api/Application/Servers/GetServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/GetServerRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetServerRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_SERVERS; - - protected int $permission = AdminAcl::READ; } diff --git a/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php index df2d76cd3..e8d01a115 100644 --- a/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php +++ b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class ServerWriteRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_SERVERS; - - protected int $permission = AdminAcl::WRITE; } diff --git a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php index a9d0ecbed..6a8956729 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -2,22 +2,12 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; +use Illuminate\Support\Arr; use Pterodactyl\Models\Server; -use Illuminate\Validation\Rule; -use Illuminate\Validation\Validator; -use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Models\Objects\DeploymentObject; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class StoreServerRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_SERVERS; - - protected int $permission = AdminAcl::WRITE; - - /** - * Rules to be applied to this request. - */ public function rules(): array { $rules = Server::getRules(); @@ -26,15 +16,9 @@ class StoreServerRequest extends ApplicationApiRequest 'external_id' => $rules['external_id'], 'name' => $rules['name'], 'description' => array_merge(['nullable'], $rules['description']), - 'user' => $rules['owner_id'], - 'egg' => $rules['egg_id'], - 'docker_image' => $rules['image'], - 'startup' => $rules['startup'], - 'environment' => 'present|array', - 'skip_scripts' => 'sometimes|boolean', - 'oom_disabled' => 'sometimes|boolean', + 'owner_id' => $rules['owner_id'], + 'node_id' => $rules['node_id'], - // Resource limitations 'limits' => 'required|array', 'limits.memory' => $rules['memory'], 'limits.swap' => $rules['swap'], @@ -42,110 +26,64 @@ class StoreServerRequest extends ApplicationApiRequest 'limits.io' => $rules['io'], 'limits.threads' => $rules['threads'], 'limits.cpu' => $rules['cpu'], + 'limits.oom_killer' => 'required|boolean', - // Application Resource Limits 'feature_limits' => 'required|array', - 'feature_limits.databases' => $rules['database_limit'], 'feature_limits.allocations' => $rules['allocation_limit'], 'feature_limits.backups' => $rules['backup_limit'], + 'feature_limits.databases' => $rules['database_limit'], - // Placeholders for rules added in withValidator() function. - 'allocation.default' => '', - 'allocation.additional.*' => '', + 'allocation.default' => 'required|bail|integer|exists:allocations,id', + 'allocation.additional.*' => 'integer|exists:allocations,id', - // Automatic deployment rules - 'deploy' => 'sometimes|required|array', - 'deploy.locations' => 'array', - 'deploy.locations.*' => 'integer|min:1', - 'deploy.dedicated_ip' => 'required_with:deploy,boolean', - 'deploy.port_range' => 'array', - 'deploy.port_range.*' => 'string', - - 'start_on_completion' => 'sometimes|boolean', + 'startup' => $rules['startup'], + 'environment' => 'present|array', + 'egg_id' => $rules['egg_id'], + 'image' => $rules['image'], + 'skip_scripts' => 'present|boolean', ]; } /** - * Normalize the data into a format that can be consumed by the service. + * @param string|null $key + * @param string|array|null $default + * + * @return array */ - public function validated($key = null, $default = null): array + public function validated($key = null, $default = null) { $data = parent::validated(); - return [ + $response = [ 'external_id' => array_get($data, 'external_id'), 'name' => array_get($data, 'name'), 'description' => array_get($data, 'description'), - 'owner_id' => array_get($data, 'user'), - 'egg_id' => array_get($data, 'egg'), - 'image' => array_get($data, 'docker_image'), - 'startup' => array_get($data, 'startup'), - 'environment' => array_get($data, 'environment'), + 'owner_id' => array_get($data, 'owner_id'), + 'node_id' => array_get($data, 'node_id'), + 'memory' => array_get($data, 'limits.memory'), 'swap' => array_get($data, 'limits.swap'), 'disk' => array_get($data, 'limits.disk'), 'io' => array_get($data, 'limits.io'), - 'cpu' => array_get($data, 'limits.cpu'), 'threads' => array_get($data, 'limits.threads'), - 'skip_scripts' => array_get($data, 'skip_scripts', false), - 'allocation_id' => array_get($data, 'allocation.default'), - 'allocation_additional' => array_get($data, 'allocation.additional'), - 'start_on_completion' => array_get($data, 'start_on_completion', false), - 'database_limit' => array_get($data, 'feature_limits.databases'), + 'cpu' => array_get($data, 'limits.cpu'), + 'oom_disabled' => !array_get($data, 'limits.oom_killer'), + 'allocation_limit' => array_get($data, 'feature_limits.allocations'), 'backup_limit' => array_get($data, 'feature_limits.backups'), - 'oom_disabled' => array_get($data, 'oom_disabled'), + 'database_limit' => array_get($data, 'feature_limits.databases'), + + 'allocation_id' => array_get($data, 'allocation.default'), + 'allocation_additional' => array_get($data, 'allocation.additional'), + + 'startup' => array_get($data, 'startup'), + 'environment' => array_get($data, 'environment'), + 'egg_id' => array_get($data, 'egg_id'), + 'image' => array_get($data, 'image'), + 'skip_scripts' => array_get($data, 'skip_scripts'), + 'start_on_completion' => array_get($data, 'start_on_completion', false), ]; - } - /* - * Run validation after the rules above have been applied. - * - * @param \Illuminate\Validation\Validator $validator - */ - public function withValidator(Validator $validator): void - { - $validator->sometimes('allocation.default', [ - 'required', 'integer', 'bail', - Rule::exists('allocations', 'id')->where(function ($query) { - $query->whereNull('server_id'); - }), - ], function ($input) { - return !$input->deploy; - }); - - $validator->sometimes('allocation.additional.*', [ - 'integer', - Rule::exists('allocations', 'id')->where(function ($query) { - $query->whereNull('server_id'); - }), - ], function ($input) { - return !$input->deploy; - }); - - $validator->sometimes('deploy.locations', 'present', function ($input) { - return $input->deploy; - }); - - $validator->sometimes('deploy.port_range', 'present', function ($input) { - return $input->deploy; - }); - } - - /** - * Return a deployment object that can be passed to the server creation service. - */ - public function getDeploymentObject(): ?DeploymentObject - { - if (is_null($this->input('deploy'))) { - return null; - } - - $object = new DeploymentObject(); - $object->setDedicated($this->input('deploy.dedicated_ip', false)); - $object->setLocations($this->input('deploy.locations', [])); - $object->setPorts($this->input('deploy.port_range', [])); - - return $object; + return is_null($key) ? $response : Arr::get($response, $key, $default); } } diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php index f1c977f11..1e2f12051 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; +use Illuminate\Support\Arr; use Pterodactyl\Models\Server; use Illuminate\Support\Collection; @@ -12,7 +13,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest */ public function rules(): array { - $rules = Server::getRulesForUpdate($this->parameter('server', Server::class)); + $rules = Server::getRulesForUpdate($this->route()->parameter('server')->id); return [ 'allocation' => $rules['allocation_id'], @@ -26,7 +27,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest 'limits.threads' => $this->requiredToOptional('threads', $rules['threads'], true), 'limits.disk' => $this->requiredToOptional('disk', $rules['disk'], true), - // Legacy rules to maintain backwards compatable API support without requiring + // Legacy rules to maintain backwards compatible API support without requiring // a major version bump. // // @see https://github.com/pterodactyl/panel/issues/1500 @@ -51,8 +52,13 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest /** * Convert the allocation field into the expected format for the service handler. + * + * @param string|null $key + * @param string|array|null $default + * + * @return mixed */ - public function validated($key = null, $default = null): array + public function validated($key = null, $default = null) { $data = parent::validated(); @@ -71,13 +77,19 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest unset($data['limits']); } + if (!is_null($key)) { + return Arr::get($data, $key, $default); + } + return $data; } /** * Custom attributes to use in error message responses. + * + * @return array */ - public function attributes(): array + public function attributes() { return [ 'add_allocations' => 'allocations to add', @@ -95,9 +107,11 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest * compatability with the old API endpoint while also supporting a more correct API * call. * + * @return array + * * @see https://github.com/pterodactyl/panel/issues/1500 */ - protected function requiredToOptional(string $field, array $rules, bool $limits = false): array + protected function requiredToOptional(string $field, array $rules, bool $limits = false) { if (!in_array('required', $rules)) { return $rules; diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php index aecc1cf02..a4551edcd 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; +use Illuminate\Support\Arr; use Pterodactyl\Models\Server; class UpdateServerDetailsRequest extends ServerWriteRequest @@ -11,7 +12,7 @@ class UpdateServerDetailsRequest extends ServerWriteRequest */ public function rules(): array { - $rules = Server::getRulesForUpdate($this->parameter('server', Server::class)); + $rules = Server::getRulesForUpdate($this->route()->parameter('server')->id); return [ 'external_id' => $rules['external_id'], @@ -24,19 +25,24 @@ class UpdateServerDetailsRequest extends ServerWriteRequest /** * Convert the posted data into the correct format that is expected * by the application. + * + * @param string|null $key + * @param string|array|null $default */ - public function validated($key = null, $default = null): array + public function validated($key = null, $default = null) { - return [ + $data = [ 'external_id' => $this->input('external_id'), 'name' => $this->input('name'), 'owner_id' => $this->input('user'), 'description' => $this->input('description'), ]; + + return is_null($key) ? $data : Arr::get($data, $key, $default); } /** - * Rename some attributes in error messages to clarify the field + * Rename some of the attributes in error messages to clarify the field * being discussed. */ public function attributes(): array diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php new file mode 100644 index 000000000..a588c381c --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php @@ -0,0 +1,77 @@ + $rules['external_id'], + 'name' => $rules['name'], + 'description' => array_merge(['nullable'], $rules['description']), + 'owner_id' => $rules['owner_id'], + + 'limits' => 'sometimes|array', + 'limits.memory' => $rules['memory'], + 'limits.swap' => $rules['swap'], + 'limits.disk' => $rules['disk'], + 'limits.io' => $rules['io'], + 'limits.threads' => $rules['threads'], + 'limits.cpu' => $rules['cpu'], + 'limits.oom_killer' => 'sometimes|boolean', + + 'feature_limits' => 'required|array', + 'feature_limits.allocations' => $rules['allocation_limit'], + 'feature_limits.backups' => $rules['backup_limit'], + 'feature_limits.databases' => $rules['database_limit'], + + 'allocation_id' => 'bail|exists:allocations,id', + 'add_allocations' => 'bail|array', + 'add_allocations.*' => 'integer', + 'remove_allocations' => 'bail|array', + 'remove_allocations.*' => 'integer', + ]; + } + + /** + * @param string|null $key + * @param string|array|null $default + * + * @return mixed + */ + public function validated($key = null, $default = null) + { + $data = parent::validated(); + $response = [ + 'external_id' => array_get($data, 'external_id'), + 'name' => array_get($data, 'name'), + 'description' => array_get($data, 'description'), + 'owner_id' => array_get($data, 'owner_id'), + + 'memory' => array_get($data, 'limits.memory'), + 'swap' => array_get($data, 'limits.swap'), + 'disk' => array_get($data, 'limits.disk'), + 'io' => array_get($data, 'limits.io'), + 'threads' => array_get($data, 'limits.threads'), + 'cpu' => array_get($data, 'limits.cpu'), + 'oom_disabled' => !array_get($data, 'limits.oom_killer'), + + 'allocation_limit' => array_get($data, 'feature_limits.allocations'), + 'backup_limit' => array_get($data, 'feature_limits.backups'), + 'database_limit' => array_get($data, 'feature_limits.databases'), + + 'allocation_id' => array_get($data, 'allocation_id'), + 'add_allocations' => array_get($data, 'add_allocations'), + 'remove_allocations' => array_get($data, 'remove_allocations'), + ]; + + return is_null($key) ? $response : Arr::get($response, $key, $default); + } +} diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php index 985b10a6f..d8f92a1f8 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php @@ -3,41 +3,20 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; use Pterodactyl\Models\Server; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class UpdateServerStartupRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_SERVERS; - - protected int $permission = AdminAcl::WRITE; - - /** - * Validation rules to run the input against. - */ public function rules(): array { - $data = Server::getRulesForUpdate($this->parameter('server', Server::class)); + $rules = Server::getRulesForUpdate($this->route()->parameter('server')->id); return [ - 'startup' => $data['startup'], + 'startup' => $rules['startup'], 'environment' => 'present|array', - 'egg' => $data['egg_id'], - 'image' => $data['image'], + 'egg_id' => $rules['egg_id'], + 'image' => $rules['image'], 'skip_scripts' => 'present|boolean', ]; } - - /** - * Return the validated data in a format that is expected by the service. - */ - public function validated($key = null, $default = null): array - { - $data = parent::validated(); - - return collect($data)->only(['startup', 'environment', 'skip_scripts'])->merge([ - 'egg_id' => array_get($data, 'egg'), - 'docker_image' => array_get($data, 'image'), - ])->toArray(); - } } diff --git a/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php b/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php index 5e840a1c0..a2e3841fb 100644 --- a/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Users; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class DeleteUserRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_USERS; - - protected int $permission = AdminAcl::WRITE; } diff --git a/app/Http/Requests/Api/Application/Users/GetExternalUserRequest.php b/app/Http/Requests/Api/Application/Users/GetExternalUserRequest.php index 0f44aed3f..b26ef7661 100644 --- a/app/Http/Requests/Api/Application/Users/GetExternalUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/GetExternalUserRequest.php @@ -2,12 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Users; -use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetExternalUserRequest extends ApplicationApiRequest { - protected ?string $resource = AdminAcl::RESOURCE_USERS; - - protected int $permission = AdminAcl::READ; } diff --git a/app/Http/Requests/Api/Application/Users/GetUserRequest.php b/app/Http/Requests/Api/Application/Users/GetUserRequest.php new file mode 100644 index 000000000..4e16088a5 --- /dev/null +++ b/app/Http/Requests/Api/Application/Users/GetUserRequest.php @@ -0,0 +1,7 @@ +only([ + return collect($rules)->only([ 'external_id', 'email', 'username', 'password', - 'language', + 'admin_role_id', 'root_admin', ])->toArray(); - - $response['first_name'] = $rules['name_first']; - $response['last_name'] = $rules['name_last']; - - return $response; - } - - public function validated($key = null, $default = null): array - { - $data = parent::validated(); - - $data['name_first'] = $data['first_name']; - $data['name_last'] = $data['last_name']; - - unset($data['first_name'], $data['last_name']); - - return $data; - } - - /** - * Rename some fields to be more user friendly. - */ - public function attributes(): array - { - return [ - 'external_id' => 'Third Party Identifier', - 'name_first' => 'First Name', - 'name_last' => 'Last Name', - 'root_admin' => 'Root Administrator Status', - ]; } } diff --git a/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php b/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php index fa2e1291c..ba6d9da2c 100644 --- a/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php @@ -6,13 +6,8 @@ use Pterodactyl\Models\User; class UpdateUserRequest extends StoreUserRequest { - /** - * Return the validation rules for this request. - */ public function rules(array $rules = null): array { - $userId = $this->parameter('user', User::class)->id; - - return parent::rules(User::getRulesForUpdate($userId)); + return parent::rules($rules ?? User::getRulesForUpdate($this->route()->parameter('user')->id)); } } diff --git a/app/Models/AdminRole.php b/app/Models/AdminRole.php new file mode 100644 index 000000000..51b485495 --- /dev/null +++ b/app/Models/AdminRole.php @@ -0,0 +1,68 @@ + 'int', + 'permissions' => 'array', + ]; + + public static array $validationRules = [ + 'name' => 'required|string|max:64', + 'description' => 'nullable|string|max:255', + 'sort_id' => 'sometimes|numeric', + ]; + + /** + * @var bool + */ + public $timestamps = false; + + /** + * Gets the permissions associated with a admin role. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function permissions() + { + return $this->hasMany(Permission::class); + } +} diff --git a/app/Models/AuditLog.php b/app/Models/AuditLog.php deleted file mode 100644 index f42acbfff..000000000 --- a/app/Models/AuditLog.php +++ /dev/null @@ -1,80 +0,0 @@ - 'required|uuid', - 'action' => 'required|string|max:191', - 'subaction' => 'nullable|string|max:191', - 'device' => 'array', - 'device.ip_address' => 'ip', - 'device.user_agent' => 'string', - 'metadata' => 'array', - ]; - - protected $table = 'audit_logs'; - - protected bool $immutableDates = true; - - protected $casts = [ - 'is_system' => 'bool', - 'device' => 'array', - 'metadata' => 'array', - ]; - - protected $guarded = [ - 'id', - 'created_at', - ]; - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - - public function server(): BelongsTo - { - return $this->belongsTo(Server::class); - } - - /** - * Creates a new AuditLog model and returns it, attaching device information and the - * currently authenticated user if available. This model is not saved at this point, so - * you can always make modifications to it as needed before saving. - * - * @deprecated - */ - public static function instance(string $action, array $metadata, bool $isSystem = false): self - { - /** @var \Illuminate\Http\Request $request */ - $request = Container::getInstance()->make('request'); - if ($isSystem || !$request instanceof Request) { - $request = null; - } - - return (new self())->fill([ - 'uuid' => Uuid::uuid4()->toString(), - 'is_system' => $isSystem, - 'user_id' => ($request && $request->user()) ? $request->user()->id : null, - 'server_id' => null, - 'action' => $action, - 'device' => $request ? [ - 'ip_address' => $request->getClientIp() ?? '127.0.0.1', - 'user_agent' => $request->userAgent() ?? '', - ] : [], - 'metadata' => $metadata, - ]); - } -} diff --git a/app/Models/Backup.php b/app/Models/Backup.php index 41c94fee2..500205b13 100644 --- a/app/Models/Backup.php +++ b/app/Models/Backup.php @@ -22,7 +22,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @property \Carbon\CarbonImmutable $updated_at * @property \Carbon\CarbonImmutable|null $deleted_at * @property \Pterodactyl\Models\Server $server - * @property \Pterodactyl\Models\AuditLog[] $audits */ class Backup extends Model { diff --git a/app/Models/Server.php b/app/Models/Server.php index 5f2d6a49e..e8560512a 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -35,7 +35,7 @@ use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException; * @property int $allocation_id * @property int $nest_id * @property int $egg_id - * @property string $startup + * @property string|null $startup * @property string $image * @property int|null $allocation_limit * @property int|null $database_limit @@ -55,6 +55,7 @@ use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException; * @property \Pterodactyl\Models\Egg|null $egg * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Mount[] $mounts * @property int|null $mounts_count + * @property \Pterodactyl\Models\Location $location * @property \Pterodactyl\Models\Nest $nest * @property \Pterodactyl\Models\Node $node * @property \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications diff --git a/app/Models/User.php b/app/Models/User.php index bef6da781..50361af8b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Builder; use Pterodactyl\Models\Traits\HasAccessTokens; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Relations\HasOne; use Pterodactyl\Traits\Helpers\AvailableLanguages; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\Access\Authorizable; @@ -29,11 +30,10 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification; * @property string $uuid * @property string $username * @property string $email - * @property string|null $name_first - * @property string|null $name_last * @property string $password * @property string|null $remember_token * @property string $language + * @property int|null $admin_role_id * @property bool $root_admin * @property bool $use_totp * @property string|null $totp_secret @@ -119,8 +119,6 @@ class User extends Model implements 'external_id', 'username', 'email', - 'name_first', - 'name_last', 'password', 'language', 'use_totp', @@ -169,8 +167,6 @@ class User extends Model implements 'email' => 'required|email|between:1,191|unique:users,email', 'external_id' => 'sometimes|nullable|string|max:191|unique:users,external_id', 'username' => 'required|between:1,191|unique:users,username', - 'name_first' => 'required|string|between:1,191', - 'name_last' => 'required|string|between:1,191', 'password' => 'sometimes|nullable|string', 'root_admin' => 'boolean', 'language' => 'string', @@ -193,11 +189,15 @@ class User extends Model implements } /** - * Return the user model in a format that can be passed over to Vue templates. + * Return the user model in a format that can be passed over to React templates. */ - public function toVueObject(): array + public function toReactObject(): array { - return Collection::make($this->toArray())->except(['id', 'external_id'])->toArray(); + $object = Collection::make($this->toArray())->except(['id', 'external_id'])->toArray(); + $object['avatar_url'] = $this->avatarURL(); + $object['role_name'] = $this->adminRoleName(); + + return $object; } /** @@ -231,6 +231,29 @@ class User extends Model implements return trim($this->name_first . ' ' . $this->name_last); } + public function avatarURL(): string + { + return 'https://www.gravatar.com/avatar/' . md5($this->email) . '.jpg'; + } + + /** + * Gets the name of the role assigned to a user. + */ + public function adminRoleName(): ?string + { + $role = $this->adminRole; + if (is_null($role)) { + return $this->root_admin ? 'None' : null; + } + + return $role->name; + } + + public function adminRole(): HasOne + { + return $this->hasOne(AdminRole::class, 'id', 'admin_role_id'); + } + /** * Returns all servers that a user owns. */ diff --git a/app/Policies/.gitkeep b/app/Policies/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c008b0d54..1bb94d258 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -6,8 +6,6 @@ use Pterodactyl\Models; use Illuminate\Support\Str; use Illuminate\Support\Facades\URL; use Illuminate\Pagination\Paginator; -use Illuminate\Support\Facades\View; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; use Illuminate\Database\Eloquent\Relations\Relation; @@ -21,9 +19,6 @@ class AppServiceProvider extends ServiceProvider { Schema::defaultStringLength(191); - View::share('appVersion', $this->versionData()['version'] ?? 'undefined'); - View::share('appIsGit', $this->versionData()['is_git'] ?? false); - Paginator::useBootstrap(); // If the APP_URL value is set with https:// make sure we force it here. Theoretically @@ -61,32 +56,4 @@ class AppServiceProvider extends ServiceProvider $this->app->register(SettingsServiceProvider::class); } } - - /** - * Return version information for the footer. - */ - protected function versionData(): array - { - return Cache::remember('git-version', 5, function () { - if (file_exists(base_path('.git/HEAD'))) { - $head = explode(' ', file_get_contents(base_path('.git/HEAD'))); - - if (array_key_exists(1, $head)) { - $path = base_path('.git/' . trim($head[1])); - } - } - - if (isset($path) && file_exists($path)) { - return [ - 'version' => substr(file_get_contents($path), 0, 8), - 'is_git' => true, - ]; - } - - return [ - 'version' => config('app.version'), - 'is_git' => false, - ]; - }); - } } diff --git a/app/Services/Eggs/EggParserService.php b/app/Services/Eggs/EggParserService.php index 6d8545bc2..c442b8e09 100644 --- a/app/Services/Eggs/EggParserService.php +++ b/app/Services/Eggs/EggParserService.php @@ -4,7 +4,6 @@ namespace Pterodactyl\Services\Eggs; use Illuminate\Support\Arr; use Pterodactyl\Models\Egg; -use Illuminate\Http\UploadedFile; use Illuminate\Support\Collection; use Pterodactyl\Exceptions\Service\InvalidFileUploadException; @@ -13,17 +12,10 @@ class EggParserService /** * Takes an uploaded file and parses out the egg configuration from within. * - * @throws \JsonException * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException */ - public function handle(UploadedFile $file): array + public function handle(array $parsed): array { - if ($file->getError() !== UPLOAD_ERR_OK || !$file->isFile()) { - throw new InvalidFileUploadException('The selected file is not valid and cannot be imported.'); - } - - /** @var array $parsed */ - $parsed = json_decode($file->openFile()->fread($file->getSize()), true, 512, JSON_THROW_ON_ERROR); if (!in_array(Arr::get($parsed, 'meta.version') ?? '', ['PTDL_v1', 'PTDL_v2'])) { throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.'); } diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index ecd6eadb6..8be37f0ab 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -6,28 +6,107 @@ use Ramsey\Uuid\Uuid; use Illuminate\Support\Arr; use Pterodactyl\Models\Egg; use Pterodactyl\Models\Nest; +use Symfony\Component\Yaml\Yaml; use Illuminate\Http\UploadedFile; use Pterodactyl\Models\EggVariable; use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Eggs\EggParserService; +use Symfony\Component\Yaml\Exception\ParseException; +use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException; +use Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException; +use Pterodactyl\Exceptions\Service\InvalidFileUploadException; class EggImporterService { - public function __construct(protected ConnectionInterface $connection, protected EggParserService $parser) - { + public function __construct( + private ConnectionInterface $connection, + private EggParserService $eggParserService + ) { } /** * Take an uploaded JSON file and parse it into a new egg. * - * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException|\Throwable + * @deprecated use `handleFile` or `handleContent` instead + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException + * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function handle(UploadedFile $file, int $nest): Egg + public function handle(UploadedFile $file, int $nestId): Egg { - $parsed = $this->parser->handle($file); + return $this->handleFile($nestId, $file); + } + + /** + * ? + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException + * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function handleFile(int $nestId, UploadedFile $file): Egg + { + if ($file->getError() !== UPLOAD_ERR_OK || !$file->isFile()) { + throw new InvalidFileUploadException(sprintf('The selected file ["%s"] was not in a valid format to import. (is_file: %s is_valid: %s err_code: %s err: %s)', $file->getFilename(), $file->isFile() ? 'true' : 'false', $file->isValid() ? 'true' : 'false', $file->getError(), $file->getErrorMessage())); + } + + return $this->handleContent($nestId, $file->openFile()->fread($file->getSize()), 'application/json'); + } + + /** + * ? + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handleContent(int $nestId, string $content, string $contentType): Egg + { + switch (true) { + case str_starts_with($contentType, 'application/json'): + $parsed = json_decode($content, true); + if (json_last_error() !== 0) { + throw new BadJsonFormatException(trans('exceptions.nest.importer.json_error', ['error' => json_last_error_msg()])); + } + + return $this->handleArray($nestId, $parsed); + case str_starts_with($contentType, 'application/yaml'): + try { + $parsed = Yaml::parse($content); + + return $this->handleArray($nestId, $parsed); + } catch (ParseException $exception) { + throw new BadYamlFormatException('There was an error while attempting to parse the YAML: ' . $exception->getMessage() . '.'); + } + default: + throw new DisplayException('unknown content type'); + } + } + + /** + * ? + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException + */ + private function handleArray(int $nestId, array $parsed): Egg + { + $parsed = $this->eggParserService->handle($parsed); /** @var \Pterodactyl\Models\Nest $nest */ - $nest = Nest::query()->with('eggs', 'eggs.variables')->findOrFail($nest); + $nest = Nest::query()->with('eggs', 'eggs.variables')->findOrFail($nestId); return $this->connection->transaction(function () use ($nest, $parsed) { $egg = (new Egg())->forceFill([ diff --git a/app/Services/Eggs/Sharing/EggUpdateImporterService.php b/app/Services/Eggs/Sharing/EggUpdateImporterService.php index 89a1f9287..ad17a16f0 100644 --- a/app/Services/Eggs/Sharing/EggUpdateImporterService.php +++ b/app/Services/Eggs/Sharing/EggUpdateImporterService.php @@ -8,14 +8,18 @@ use Illuminate\Support\Collection; use Pterodactyl\Models\EggVariable; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Services\Eggs\EggParserService; +use Pterodactyl\Exceptions\Service\InvalidFileUploadException; +use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException; class EggUpdateImporterService { /** * EggUpdateImporterService constructor. */ - public function __construct(protected ConnectionInterface $connection, protected EggParserService $parser) - { + public function __construct( + private ConnectionInterface $connection, + private EggParserService $eggParserService + ) { } /** @@ -25,10 +29,18 @@ class EggUpdateImporterService */ public function handle(Egg $egg, UploadedFile $file): Egg { - $parsed = $this->parser->handle($file); + if ($file->getError() !== UPLOAD_ERR_OK || !$file->isFile()) { + throw new InvalidFileUploadException(sprintf('The selected file ["%s"] was not in a valid format to import. (is_file: %s is_valid: %s err_code: %s err: %s)', $file->getFilename(), $file->isFile() ? 'true' : 'false', $file->isValid() ? 'true' : 'false', $file->getError(), $file->getErrorMessage())); + } + + $parsed = json_decode($file->openFile()->fread($file->getSize()), true); + if (json_last_error() !== 0) { + throw new BadJsonFormatException(trans('exceptions.nest.importer.json_error', ['error' => json_last_error_msg()])); + } + $parsed = $this->eggParserService->handle($parsed); return $this->connection->transaction(function () use ($egg, $parsed) { - $egg = $this->parser->fillFromParsed($egg, $parsed); + $egg = $this->eggParserService->fillFromParsed($egg, $parsed); $egg->save(); // Update existing variables or create new ones. diff --git a/app/Services/Eggs/Variables/VariableUpdateService.php b/app/Services/Eggs/Variables/VariableUpdateService.php index ed70b260f..5d380f4ac 100644 --- a/app/Services/Eggs/Variables/VariableUpdateService.php +++ b/app/Services/Eggs/Variables/VariableUpdateService.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Services\Eggs\Variables; use Illuminate\Support\Str; +use Pterodactyl\Models\Egg; use Pterodactyl\Models\EggVariable; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Traits\Services\ValidatesValidationRules; @@ -34,24 +35,21 @@ class VariableUpdateService * Update a specific egg variable. * * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException */ - public function handle(EggVariable $variable, array $data): mixed + public function handle(Egg $egg, array $data): void { if (!is_null(array_get($data, 'env_variable'))) { if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', EggVariable::RESERVED_ENV_NAMES))) { throw new ReservedVariableNameException(trans('exceptions.service.variables.reserved_name', ['name' => array_get($data, 'env_variable')])); } - $search = $this->repository->setColumns('id')->findCountWhere([ - ['env_variable', '=', $data['env_variable']], - ['egg_id', '=', $variable->egg_id], - ['id', '!=', $variable->id], - ]); + $count = $egg->variables() + ->where('egg_variables.env_variable', $data['env_variable']) + ->where('egg_variables.id', '!=', $data['id']) + ->count(); - if ($search > 0) { + if ($count > 0) { throw new DisplayException(trans('exceptions.service.variables.env_not_unique', ['name' => array_get($data, 'env_variable')])); } } @@ -66,13 +64,13 @@ class VariableUpdateService $options = array_get($data, 'options') ?? []; - return $this->repository->withoutFreshModel()->update($variable->id, [ + $egg->variables()->where('egg_variables.id', $data['id'])->update([ 'name' => $data['name'] ?? '', 'description' => $data['description'] ?? '', 'env_variable' => $data['env_variable'] ?? '', 'default_value' => $data['default_value'] ?? '', - 'user_viewable' => in_array('user_viewable', $options), - 'user_editable' => in_array('user_editable', $options), + 'user_viewable' => $data['user_viewable'], + 'user_editable' => $data['user_editable'], 'rules' => $data['rules'] ?? '', ]); } diff --git a/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php index 2122b397b..5b8a5aa80 100644 --- a/app/Services/Helpers/SoftwareVersionService.php +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -3,46 +3,54 @@ namespace Pterodactyl\Services\Helpers; use Exception; -use GuzzleHttp\Client; use Carbon\CarbonImmutable; use Illuminate\Support\Arr; +use Illuminate\Support\Str; +use Illuminate\Support\Facades\Http; use Illuminate\Contracts\Cache\Repository as CacheRepository; use Pterodactyl\Exceptions\Service\Helper\CdnVersionFetchingException; class SoftwareVersionService { public const VERSION_CACHE_KEY = 'pterodactyl:versioning_data'; + public const GIT_VERSION_CACHE_KEY = 'pterodactyl:git_data'; private static array $result; /** * SoftwareVersionService constructor. */ - public function __construct( - protected CacheRepository $cache, - protected Client $client - ) { + public function __construct(private CacheRepository $cache) + { self::$result = $this->cacheVersionData(); } /** - * Get the latest version of the panel from the CDN servers. + * Return the current version of the panel that is being used. */ - public function getPanel(): string + public function getCurrentVersion(): string + { + return config('app.version'); + } + + /** + * Returns the latest version of the panel from the CDN servers. + */ + public function getLatestPanel(): string { return Arr::get(self::$result, 'panel') ?? 'error'; } /** - * Get the latest version of the daemon from the CDN servers. + * Returns the latest version of the Wings from the CDN servers. */ - public function getDaemon(): string + public function getLatestWings(): string { return Arr::get(self::$result, 'wings') ?? 'error'; } /** - * Get the URL to the discord server. + * Returns the URL to the discord server. */ public function getDiscord(): string { @@ -50,7 +58,7 @@ class SoftwareVersionService } /** - * Get the URL for donations. + * Returns the URL for donations. */ public function getDonations(): string { @@ -62,23 +70,80 @@ class SoftwareVersionService */ public function isLatestPanel(): bool { - if (config('app.version') === 'canary') { + $version = $this->getCurrentVersion(); + if ($version === 'canary') { return true; } - return version_compare(config('app.version'), $this->getPanel()) >= 0; + return version_compare($version, $this->getLatestPanel()) >= 0; } /** * Determine if a passed daemon version string is the latest. */ - public function isLatestDaemon(string $version): bool + public function isLatestWings(string $version): bool { - if ($version === 'develop') { + if ($version === 'develop' || Str::startsWith($version, 'dev-')) { return true; } - return version_compare($version, $this->getDaemon()) >= 0; + return version_compare($version, $this->getLatestWings()) >= 0; + } + + /** + * ? + */ + public function getVersionData(): array + { + $versionData = $this->versionData(); + if ($versionData['is_git']) { + $git = $versionData['version']; + } else { + $git = null; + } + + return [ + 'panel' => [ + 'current' => $this->getCurrentVersion(), + 'latest' => $this->getLatestPanel(), + ], + + 'wings' => [ + 'latest' => $this->getLatestWings(), + ], + + 'git' => $git, + ]; + } + + /** + * Return version information for the footer. + */ + protected function versionData(): array + { + return $this->cache->remember(self::GIT_VERSION_CACHE_KEY, CarbonImmutable::now()->addSeconds(15), function () { + $configVersion = config()->get('app.version'); + + if (file_exists(base_path('.git/HEAD'))) { + $head = explode(' ', file_get_contents(base_path('.git/HEAD'))); + + if (array_key_exists(1, $head)) { + $path = base_path('.git/' . trim($head[1])); + } + } + + if (isset($path) && file_exists($path)) { + return [ + 'version' => substr(file_get_contents($path), 0, 8), + 'is_git' => true, + ]; + } + + return [ + 'version' => $configVersion, + 'is_git' => false, + ]; + }); } /** @@ -88,10 +153,10 @@ class SoftwareVersionService { return $this->cache->remember(self::VERSION_CACHE_KEY, CarbonImmutable::now()->addMinutes(config('pterodactyl.cdn.cache_time', 60)), function () { try { - $response = $this->client->request('GET', config('pterodactyl.cdn.url')); + $response = Http::get(config('pterodactyl.cdn.url')); - if ($response->getStatusCode() === 200) { - return json_decode($response->getBody(), true); + if ($response->status() === 200) { + return json_decode($response->body(), true); } throw new CdnVersionFetchingException(); diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index b7a22fdaa..b068933b3 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -49,9 +49,9 @@ class BuildModificationService $merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']); $server->forceFill(array_merge($merge, [ - 'database_limit' => Arr::get($data, 'database_limit', 0) ?? null, 'allocation_limit' => Arr::get($data, 'allocation_limit', 0) ?? null, 'backup_limit' => Arr::get($data, 'backup_limit', 0) ?? 0, + 'database_limit' => Arr::get($data, 'database_limit', 0) ?? null, ]))->saveOrFail(); return $server->refresh(); diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index 72f327796..af6ba906f 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -50,7 +50,7 @@ class ServerConfigurationStructureService ], 'suspended' => $server->isSuspended(), 'environment' => $this->environment->handle($server), - 'invocation' => $server->startup, + 'invocation' => !is_null($server->startup) ? $server->startup : $server->egg->startup, 'skip_egg_scripts' => $server->skip_scripts, 'build' => [ 'memory_limit' => $server->memory, @@ -63,18 +63,13 @@ class ServerConfigurationStructureService ], 'container' => [ 'image' => $server->image, - // This field is deprecated — use the value in the "build" block. - // - // TODO: remove this key in V2. - 'oom_disabled' => $server->oom_disabled, - 'requires_rebuild' => false, ], 'allocations' => [ - 'force_outgoing_ip' => $server->egg->force_outgoing_ip, 'default' => [ 'ip' => $server->allocation->ip, 'port' => $server->allocation->port, ], + 'force_outgoing_ip' => $server->egg->force_outgoing_ip, 'mappings' => $server->getAllocationMappings(), ], 'mounts' => $server->mounts->map(function (Mount $mount) { diff --git a/app/Transformers/Api/Application/AdminRoleTransformer.php b/app/Transformers/Api/Application/AdminRoleTransformer.php new file mode 100644 index 000000000..7a561528a --- /dev/null +++ b/app/Transformers/Api/Application/AdminRoleTransformer.php @@ -0,0 +1,29 @@ + $model->id, + 'name' => $model->name, + 'description' => $model->description, + ]; + } +} diff --git a/app/Transformers/Api/Application/AllocationTransformer.php b/app/Transformers/Api/Application/AllocationTransformer.php index fcd65f98f..4aafc99f8 100644 --- a/app/Transformers/Api/Application/AllocationTransformer.php +++ b/app/Transformers/Api/Application/AllocationTransformer.php @@ -2,18 +2,14 @@ namespace Pterodactyl\Transformers\Api\Application; -use Pterodactyl\Models\Node; -use Pterodactyl\Models\Server; use League\Fractal\Resource\Item; use Pterodactyl\Models\Allocation; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; -class AllocationTransformer extends BaseTransformer +class AllocationTransformer extends Transformer { - /** - * Relationships that can be loaded onto allocation transformations. - */ protected array $availableIncludes = ['node', 'server']; /** @@ -27,22 +23,21 @@ class AllocationTransformer extends BaseTransformer /** * Return a generic transformed allocation array. */ - public function transform(Allocation $allocation): array + public function transform(Allocation $model): array { return [ - 'id' => $allocation->id, - 'ip' => $allocation->ip, - 'alias' => $allocation->ip_alias, - 'port' => $allocation->port, - 'notes' => $allocation->notes, - 'assigned' => !is_null($allocation->server_id), + 'id' => $model->id, + 'ip' => $model->ip, + 'alias' => $model->ip_alias, + 'port' => $model->port, + 'notes' => $model->notes, + 'server_id' => $model->server_id, + 'assigned' => !is_null($model->server_id), ]; } /** * Load the node relationship onto a given transformation. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeNode(Allocation $allocation): Item|NullResource { @@ -50,17 +45,11 @@ class AllocationTransformer extends BaseTransformer return $this->null(); } - return $this->item( - $allocation->node, - $this->makeTransformer(NodeTransformer::class), - Node::RESOURCE_NAME - ); + return $this->item($allocation->node, new NodeTransformer()); } /** * Load the server relationship onto a given transformation. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeServer(Allocation $allocation): Item|NullResource { @@ -68,10 +57,6 @@ class AllocationTransformer extends BaseTransformer return $this->null(); } - return $this->item( - $allocation->server, - $this->makeTransformer(ServerTransformer::class), - Server::RESOURCE_NAME - ); + return $this->item($allocation->server, new ServerTransformer()); } } diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php deleted file mode 100644 index 723caa3b8..000000000 --- a/app/Transformers/Api/Application/BaseTransformer.php +++ /dev/null @@ -1,114 +0,0 @@ -call([$this, 'handle']); - } - } - - /** - * Return the resource name for the JSONAPI output. - */ - abstract public function getResourceName(): string; - - /** - * Sets the request on the instance. - */ - public function setRequest(Request $request): self - { - $this->request = $request; - - return $this; - } - - /** - * Returns a new transformer instance with the request set on the instance. - */ - public static function fromRequest(Request $request): BaseTransformer - { - return app(static::class)->setRequest($request); - } - - /** - * Determine if the API key loaded onto the transformer has permission - * to access a different resource. This is used when including other - * models on a transformation request. - * - * @deprecated — prefer $user->can/cannot methods - */ - protected function authorize(string $resource): bool - { - $allowed = [ApiKey::TYPE_ACCOUNT, ApiKey::TYPE_APPLICATION]; - - $token = $this->request->user()->currentAccessToken(); - if (!$token instanceof ApiKey || !in_array($token->key_type, $allowed)) { - return false; - } - - // If this is not a deprecated application token type we can only check that - // the user is a root admin at the moment. In a future release we'll be rolling - // out more specific permissions for keys. - if ($token->key_type === ApiKey::TYPE_ACCOUNT) { - return $this->request->user()->root_admin; - } - - return AdminAcl::check($token, $resource); - } - - /** - * Create a new instance of the transformer and pass along the currently - * set API key. - * - * @template T of \Pterodactyl\Transformers\Api\Application\BaseTransformer - * - * @param class-string $abstract - * - * @return T - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException - * - * @noinspection PhpDocSignatureInspection - */ - protected function makeTransformer(string $abstract) - { - Assert::subclassOf($abstract, self::class); - - return $abstract::fromRequest($this->request); - } - - /** - * Return an ISO-8601 formatted timestamp to use in the API response. - */ - protected function formatTimestamp(string $timestamp): string - { - return CarbonImmutable::createFromFormat(CarbonInterface::DEFAULT_TO_STRING_FORMAT, $timestamp) - ->setTimezone(self::RESPONSE_TIMEZONE) - ->toAtomString(); - } -} diff --git a/app/Transformers/Api/Application/DatabaseHostTransformer.php b/app/Transformers/Api/Application/DatabaseHostTransformer.php index 019fdf261..c69f9dff3 100644 --- a/app/Transformers/Api/Application/DatabaseHostTransformer.php +++ b/app/Transformers/Api/Application/DatabaseHostTransformer.php @@ -2,17 +2,15 @@ namespace Pterodactyl\Transformers\Api\Application; -use Pterodactyl\Models\Database; use Pterodactyl\Models\DatabaseHost; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; -class DatabaseHostTransformer extends BaseTransformer +class DatabaseHostTransformer extends Transformer { - protected array $availableIncludes = [ - 'databases', - ]; + protected array $availableIncludes = ['databases']; /** * Return the resource name for the JSONAPI output. @@ -33,16 +31,14 @@ class DatabaseHostTransformer extends BaseTransformer 'host' => $model->host, 'port' => $model->port, 'username' => $model->username, - 'node' => $model->node_id, - 'created_at' => $model->created_at->toAtomString(), - 'updated_at' => $model->updated_at->toAtomString(), + 'node_id' => $model->node_id, + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), ]; } /** * Include the databases associated with this host. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeDatabases(DatabaseHost $model): Collection|NullResource { @@ -50,8 +46,7 @@ class DatabaseHostTransformer extends BaseTransformer return $this->null(); } - $model->loadMissing('databases'); - - return $this->collection($model->getRelation('databases'), $this->makeTransformer(ServerDatabaseTransformer::class), Database::RESOURCE_NAME); + // TODO + return $this->collection($model->databases, new ServerDatabaseTransformer()); } } diff --git a/app/Transformers/Api/Application/EggTransformer.php b/app/Transformers/Api/Application/EggTransformer.php index 9ed5736b4..c98a5d16a 100644 --- a/app/Transformers/Api/Application/EggTransformer.php +++ b/app/Transformers/Api/Application/EggTransformer.php @@ -2,26 +2,23 @@ namespace Pterodactyl\Transformers\Api\Application; -use Illuminate\Support\Arr; use Pterodactyl\Models\Egg; -use Pterodactyl\Models\Nest; -use Pterodactyl\Models\Server; use League\Fractal\Resource\Item; -use Pterodactyl\Models\EggVariable; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; -class EggTransformer extends BaseTransformer +class EggTransformer extends Transformer { /** * Relationships that can be loaded onto this transformation. */ protected array $availableIncludes = [ - 'nest', - 'servers', 'config', + 'nest', 'script', + 'servers', 'variables', ]; @@ -50,19 +47,14 @@ class EggTransformer extends BaseTransformer 'id' => $model->id, 'uuid' => $model->uuid, 'name' => $model->name, - 'nest' => $model->nest_id, + 'nest_id' => $model->nest_id, 'author' => $model->author, 'description' => $model->description, - // "docker_image" is deprecated, but left here to avoid breaking too many things at once - // in external software. We'll remove it down the road once things have gotten the chance - // to upgrade to using "docker_images". - 'docker_image' => count($model->docker_images) > 0 ? Arr::first($model->docker_images) : '', 'docker_images' => $model->docker_images, 'config' => [ 'files' => $files, 'startup' => json_decode($model->config_startup, true), 'stop' => $model->config_stop, - 'logs' => json_decode($model->config_logs, true), 'file_denylist' => $model->file_denylist, 'extends' => $model->config_from, ], @@ -74,43 +66,11 @@ class EggTransformer extends BaseTransformer 'container' => $model->script_container, 'extends' => $model->copy_script_from, ], - $model->getCreatedAtColumn() => $this->formatTimestamp($model->created_at), - $model->getUpdatedAtColumn() => $this->formatTimestamp($model->updated_at), + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), ]; } - /** - * Include the Nest relationship for the given Egg in the transformation. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeNest(Egg $model): Item|NullResource - { - if (!$this->authorize(AdminAcl::RESOURCE_NESTS)) { - return $this->null(); - } - - $model->loadMissing('nest'); - - return $this->item($model->getRelation('nest'), $this->makeTransformer(NestTransformer::class), Nest::RESOURCE_NAME); - } - - /** - * Include the Servers relationship for the given Egg in the transformation. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeServers(Egg $model): Collection|NullResource - { - if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { - return $this->null(); - } - - $model->loadMissing('servers'); - - return $this->collection($model->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), Server::RESOURCE_NAME); - } - /** * Include more detailed information about the configuration if this Egg is * extending another. @@ -121,8 +81,6 @@ class EggTransformer extends BaseTransformer return $this->null(); } - $model->loadMissing('configFrom'); - return $this->item($model, function (Egg $model) { return [ 'files' => json_decode($model->inherit_config_files), @@ -133,6 +91,18 @@ class EggTransformer extends BaseTransformer }); } + /** + * Include the Nest relationship for the given Egg in the transformation. + */ + public function includeNest(Egg $model): Item|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_NESTS)) { + return $this->null(); + } + + return $this->item($model->nest, new NestTransformer()); + } + /** * Include more detailed information about the script configuration if the * Egg is extending another. @@ -143,8 +113,6 @@ class EggTransformer extends BaseTransformer return $this->null(); } - $model->loadMissing('scriptFrom'); - return $this->item($model, function (Egg $model) { return [ 'privileged' => $model->script_is_privileged, @@ -155,10 +123,20 @@ class EggTransformer extends BaseTransformer }); } + /** + * Include the Servers relationship for the given Egg in the transformation. + */ + public function includeServers(Egg $model): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + return $this->collection($model->servers, new ServerTransformer()); + } + /** * Include the variables that are defined for this Egg. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeVariables(Egg $model): Collection|NullResource { @@ -168,10 +146,6 @@ class EggTransformer extends BaseTransformer $model->loadMissing('variables'); - return $this->collection( - $model->getRelation('variables'), - $this->makeTransformer(EggVariableTransformer::class), - EggVariable::RESOURCE_NAME - ); + return $this->collection($model->variables, new EggVariableTransformer()); } } diff --git a/app/Transformers/Api/Application/EggVariableTransformer.php b/app/Transformers/Api/Application/EggVariableTransformer.php index 2088806d5..613b5768d 100644 --- a/app/Transformers/Api/Application/EggVariableTransformer.php +++ b/app/Transformers/Api/Application/EggVariableTransformer.php @@ -4,8 +4,9 @@ namespace Pterodactyl\Transformers\Api\Application; use Pterodactyl\Models\Egg; use Pterodactyl\Models\EggVariable; +use Pterodactyl\Transformers\Api\Transformer; -class EggVariableTransformer extends BaseTransformer +class EggVariableTransformer extends Transformer { /** * Return the resource name for the JSONAPI output. @@ -15,7 +16,10 @@ class EggVariableTransformer extends BaseTransformer return Egg::RESOURCE_NAME; } - public function transform(EggVariable $model) + /** + * Transform egg variable into a representation for the application API. + */ + public function transform(EggVariable $model): array { return $model->toArray(); } diff --git a/app/Transformers/Api/Application/LocationTransformer.php b/app/Transformers/Api/Application/LocationTransformer.php index 8fea3feb3..5ef3e74d5 100644 --- a/app/Transformers/Api/Application/LocationTransformer.php +++ b/app/Transformers/Api/Application/LocationTransformer.php @@ -6,8 +6,9 @@ use Pterodactyl\Models\Location; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; -class LocationTransformer extends BaseTransformer +class LocationTransformer extends Transformer { /** * List of resources that can be included. @@ -25,37 +26,19 @@ class LocationTransformer extends BaseTransformer /** * Return a generic transformed location array. */ - public function transform(Location $location): array + public function transform(Location $model): array { return [ - 'id' => $location->id, - 'short' => $location->short, - 'long' => $location->long, - $location->getUpdatedAtColumn() => $this->formatTimestamp($location->updated_at), - $location->getCreatedAtColumn() => $this->formatTimestamp($location->created_at), + 'id' => $model->id, + 'short' => $model->short, + 'long' => $model->long, + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), ]; } /** * Return the nodes associated with this location. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeServers(Location $location): Collection|NullResource - { - if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { - return $this->null(); - } - - $location->loadMissing('servers'); - - return $this->collection($location->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server'); - } - - /** - * Return the nodes associated with this location. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeNodes(Location $location): Collection|NullResource { @@ -63,8 +46,18 @@ class LocationTransformer extends BaseTransformer return $this->null(); } - $location->loadMissing('nodes'); + return $this->collection($location->nodes, new NodeTransformer()); + } - return $this->collection($location->getRelation('nodes'), $this->makeTransformer(NodeTransformer::class), 'node'); + /** + * Return the nodes associated with this location. + */ + public function includeServers(Location $location): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + return $this->collection($location->servers, new ServerTransformer()); } } diff --git a/app/Transformers/Api/Application/MountTransformer.php b/app/Transformers/Api/Application/MountTransformer.php new file mode 100644 index 000000000..8c0d1f289 --- /dev/null +++ b/app/Transformers/Api/Application/MountTransformer.php @@ -0,0 +1,75 @@ + $model->id, + 'uuid' => $model->uuid, + 'name' => $model->name, + 'description' => $model->description, + 'source' => $model->source, + 'target' => $model->target, + 'read_only' => $model->read_only, + 'user_mountable' => $model->user_mountable, + ]; + } + + /** + * Return the eggs associated with this mount. + */ + public function includeEggs(Mount $mount): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) { + return $this->null(); + } + + return $this->collection($mount->eggs, new EggTransformer()); + } + + /** + * Return the nodes associated with this mount. + */ + public function includeNodes(Mount $mount): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_NODES)) { + return $this->null(); + } + + return $this->collection($mount->nodes, new NodeTransformer()); + } + + /** + * Return the servers associated with this mount. + */ + public function includeServers(Mount $mount): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + return $this->collection($mount->servers, new ServerTransformer()); + } +} diff --git a/app/Transformers/Api/Application/NestTransformer.php b/app/Transformers/Api/Application/NestTransformer.php index 2f530d44e..8521564c6 100644 --- a/app/Transformers/Api/Application/NestTransformer.php +++ b/app/Transformers/Api/Application/NestTransformer.php @@ -2,21 +2,18 @@ namespace Pterodactyl\Transformers\Api\Application; -use Pterodactyl\Models\Egg; use Pterodactyl\Models\Nest; -use Pterodactyl\Models\Server; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; -class NestTransformer extends BaseTransformer +class NestTransformer extends Transformer { /** * Relationships that can be loaded onto this transformation. */ - protected array $availableIncludes = [ - 'eggs', 'servers', - ]; + protected array $availableIncludes = ['eggs', 'servers']; /** * Return the resource name for the JSONAPI output. @@ -34,16 +31,14 @@ class NestTransformer extends BaseTransformer { $response = $model->toArray(); - $response[$model->getUpdatedAtColumn()] = $this->formatTimestamp($model->updated_at); - $response[$model->getCreatedAtColumn()] = $this->formatTimestamp($model->created_at); + $response['created_at'] = self::formatTimestamp($model->created_at); + $response['updated_at'] = self::formatTimestamp($model->updated_at); return $response; } /** * Include the Eggs relationship on the given Nest model transformation. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeEggs(Nest $model): Collection|NullResource { @@ -51,15 +46,11 @@ class NestTransformer extends BaseTransformer return $this->null(); } - $model->loadMissing('eggs'); - - return $this->collection($model->getRelation('eggs'), $this->makeTransformer(EggTransformer::class), Egg::RESOURCE_NAME); + return $this->collection($model->eggs, new EggTransformer()); } /** * Include the servers relationship on the given Nest model. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeServers(Nest $model): Collection|NullResource { @@ -67,8 +58,6 @@ class NestTransformer extends BaseTransformer return $this->null(); } - $model->loadMissing('servers'); - - return $this->collection($model->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), Server::RESOURCE_NAME); + return $this->collection($model->servers, new ServerTransformer()); } } diff --git a/app/Transformers/Api/Application/NodeTransformer.php b/app/Transformers/Api/Application/NodeTransformer.php index 6347dfec3..de3fd63da 100644 --- a/app/Transformers/Api/Application/NodeTransformer.php +++ b/app/Transformers/Api/Application/NodeTransformer.php @@ -7,8 +7,9 @@ use League\Fractal\Resource\Item; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; -class NodeTransformer extends BaseTransformer +class NodeTransformer extends Transformer { /** * List of resources that can be included. @@ -27,9 +28,9 @@ class NodeTransformer extends BaseTransformer * Return a node transformed into a format that can be consumed by the * external administrative API. */ - public function transform(Node $node): array + public function transform(Node $model): array { - $response = collect($node->toArray())->mapWithKeys(function ($value, $key) { + $response = collect($model->toArray())->mapWithKeys(function ($value, $key) { // I messed up early in 2016 when I named this column as poorly // as I did. This is the tragic result of my mistakes. $key = ($key === 'daemonSFTP') ? 'daemonSftp' : $key; @@ -37,10 +38,10 @@ class NodeTransformer extends BaseTransformer return [snake_case($key) => $value]; })->toArray(); - $response[$node->getUpdatedAtColumn()] = $this->formatTimestamp($node->updated_at); - $response[$node->getCreatedAtColumn()] = $this->formatTimestamp($node->created_at); + $response['created_at'] = self::formatTimestamp($model->created_at); + $response['updated_at'] = self::formatTimestamp($model->updated_at); - $resources = $node->servers()->select(['memory', 'disk'])->get(); + $resources = $model->servers()->select(['memory', 'disk'])->get(); $response['allocated_resources'] = [ 'memory' => $resources->sum('memory'), @@ -51,9 +52,7 @@ class NodeTransformer extends BaseTransformer } /** - * Return the nodes associated with this location. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + * Return the allocations associated with this node. */ public function includeAllocations(Node $node): Collection|NullResource { @@ -61,19 +60,11 @@ class NodeTransformer extends BaseTransformer return $this->null(); } - $node->loadMissing('allocations'); - - return $this->collection( - $node->getRelation('allocations'), - $this->makeTransformer(AllocationTransformer::class), - 'allocation' - ); + return $this->collection($node->allocations, new AllocationTransformer()); } /** - * Return the nodes associated with this location. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + * Return the location associated with this node. */ public function includeLocation(Node $node): Item|NullResource { @@ -81,19 +72,11 @@ class NodeTransformer extends BaseTransformer return $this->null(); } - $node->loadMissing('location'); - - return $this->item( - $node->getRelation('location'), - $this->makeTransformer(LocationTransformer::class), - 'location' - ); + return $this->item($node->location, new LocationTransformer()); } /** - * Return the nodes associated with this location. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + * Return the servers associated with this node. */ public function includeServers(Node $node): Collection|NullResource { @@ -101,12 +84,6 @@ class NodeTransformer extends BaseTransformer return $this->null(); } - $node->loadMissing('servers'); - - return $this->collection( - $node->getRelation('servers'), - $this->makeTransformer(ServerTransformer::class), - 'server' - ); + return $this->collection($node->servers, new ServerTransformer()); } } diff --git a/app/Transformers/Api/Application/ServerDatabaseTransformer.php b/app/Transformers/Api/Application/ServerDatabaseTransformer.php index 2590482d9..159ac6b2d 100644 --- a/app/Transformers/Api/Application/ServerDatabaseTransformer.php +++ b/app/Transformers/Api/Application/ServerDatabaseTransformer.php @@ -4,14 +4,14 @@ namespace Pterodactyl\Transformers\Api\Application; use Pterodactyl\Models\Database; use League\Fractal\Resource\Item; -use Pterodactyl\Models\DatabaseHost; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; use Illuminate\Contracts\Encryption\Encrypter; -class ServerDatabaseTransformer extends BaseTransformer +class ServerDatabaseTransformer extends Transformer { - protected array $availableIncludes = ['password', 'host']; + protected array $availableIncludes = ['host', 'password']; private Encrypter $encrypter; @@ -38,17 +38,29 @@ class ServerDatabaseTransformer extends BaseTransformer { return [ 'id' => $model->id, - 'server' => $model->server_id, - 'host' => $model->database_host_id, - 'database' => $model->database, + 'database_host_id' => $model->database_host_id, + 'server_id' => $model->server_id, + 'name' => $model->database, 'username' => $model->username, 'remote' => $model->remote, 'max_connections' => $model->max_connections, - 'created_at' => $model->created_at->toAtomString(), - 'updated_at' => $model->updated_at->toAtomString(), + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), ]; } + /** + * Return the database host relationship for this server database. + */ + public function includeHost(Database $model): Item|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_DATABASE_HOSTS)) { + return $this->null(); + } + + return $this->item($model->host, new DatabaseHostTransformer()); + } + /** * Include the database password in the request. */ @@ -60,24 +72,4 @@ class ServerDatabaseTransformer extends BaseTransformer ]; }, 'database_password'); } - - /** - * Return the database host relationship for this server database. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeHost(Database $model): Item|NullResource - { - if (!$this->authorize(AdminAcl::RESOURCE_DATABASE_HOSTS)) { - return $this->null(); - } - - $model->loadMissing('host'); - - return $this->item( - $model->getRelation('host'), - $this->makeTransformer(DatabaseHostTransformer::class), - DatabaseHost::RESOURCE_NAME - ); - } } diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index e5db01fb2..ddc4f638c 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -7,9 +7,10 @@ use League\Fractal\Resource\Item; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; use Pterodactyl\Services\Servers\EnvironmentService; -class ServerTransformer extends BaseTransformer +class ServerTransformer extends Transformer { private EnvironmentService $environmentService; @@ -48,53 +49,47 @@ class ServerTransformer extends BaseTransformer /** * Return a generic transformed server array. */ - public function transform(Server $server): array + public function transform(Server $model): array { return [ - 'id' => $server->getKey(), - 'external_id' => $server->external_id, - 'uuid' => $server->uuid, - 'identifier' => $server->uuidShort, - 'name' => $server->name, - 'description' => $server->description, - 'status' => $server->status, - // This field is deprecated, please use "status". - 'suspended' => $server->isSuspended(), + 'id' => $model->getKey(), + 'external_id' => $model->external_id, + 'uuid' => $model->uuid, + 'identifier' => $model->uuidShort, + 'name' => $model->name, + 'description' => $model->description, + 'status' => $model->status, 'limits' => [ - 'memory' => $server->memory, - 'swap' => $server->swap, - 'disk' => $server->disk, - 'io' => $server->io, - 'cpu' => $server->cpu, - 'threads' => $server->threads, - 'oom_disabled' => $server->oom_disabled, + 'cpu' => $model->cpu, + 'disk' => $model->disk, + 'io' => $model->io, + 'memory' => $model->memory, + 'oom_disabled' => $model->oom_disabled, + 'swap' => $model->swap, + 'threads' => $model->threads, ], 'feature_limits' => [ - 'databases' => $server->database_limit, - 'allocations' => $server->allocation_limit, - 'backups' => $server->backup_limit, + 'allocations' => $model->allocation_limit, + 'backups' => $model->backup_limit, + 'databases' => $model->database_limit, ], - 'user' => $server->owner_id, - 'node' => $server->node_id, - 'allocation' => $server->allocation_id, - 'nest' => $server->nest_id, - 'egg' => $server->egg_id, + 'user_id' => $model->owner_id, + 'node_id' => $model->node_id, + 'allocation_id' => $model->allocation_id, + 'nest_id' => $model->nest_id, + 'egg_id' => $model->egg_id, 'container' => [ - 'startup_command' => $server->startup, - 'image' => $server->image, - // This field is deprecated, please use "status". - 'installed' => $server->isInstalled() ? 1 : 0, - 'environment' => $this->environmentService->handle($server), + 'startup' => $model->startup, + 'image' => $model->image, + 'environment' => $this->environmentService->handle($model), ], - $server->getUpdatedAtColumn() => $this->formatTimestamp($server->updated_at), - $server->getCreatedAtColumn() => $this->formatTimestamp($server->created_at), + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), ]; } /** * Return a generic array of allocations for this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeAllocations(Server $server): Collection|NullResource { @@ -102,15 +97,11 @@ class ServerTransformer extends BaseTransformer return $this->null(); } - $server->loadMissing('allocations'); - - return $this->collection($server->getRelation('allocations'), $this->makeTransformer(AllocationTransformer::class), 'allocation'); + return $this->collection($server->allocations, new AllocationTransformer()); } /** * Return a generic array of data about subusers for this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeSubusers(Server $server): Collection|NullResource { @@ -118,15 +109,11 @@ class ServerTransformer extends BaseTransformer return $this->null(); } - $server->loadMissing('subusers'); - - return $this->collection($server->getRelation('subusers'), $this->makeTransformer(SubuserTransformer::class), 'subuser'); + return $this->collection($server->subusers, new SubuserTransformer()); } /** * Return a generic array of data about subusers for this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeUser(Server $server): Item|NullResource { @@ -134,15 +121,11 @@ class ServerTransformer extends BaseTransformer return $this->null(); } - $server->loadMissing('user'); - - return $this->item($server->getRelation('user'), $this->makeTransformer(UserTransformer::class), 'user'); + return $this->item($server->user, new UserTransformer()); } /** * Return a generic array with nest information for this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeNest(Server $server): Item|NullResource { @@ -150,15 +133,11 @@ class ServerTransformer extends BaseTransformer return $this->null(); } - $server->loadMissing('nest'); - - return $this->item($server->getRelation('nest'), $this->makeTransformer(NestTransformer::class), 'nest'); + return $this->item($server->nest, new NestTransformer()); } /** * Return a generic array with egg information for this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeEgg(Server $server): Item|NullResource { @@ -166,15 +145,11 @@ class ServerTransformer extends BaseTransformer return $this->null(); } - $server->loadMissing('egg'); - - return $this->item($server->getRelation('egg'), $this->makeTransformer(EggTransformer::class), 'egg'); + return $this->item($server->egg, new EggTransformer()); } /** * Return a generic array of data about subusers for this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeVariables(Server $server): Collection|NullResource { @@ -182,15 +157,11 @@ class ServerTransformer extends BaseTransformer return $this->null(); } - $server->loadMissing('variables'); - - return $this->collection($server->getRelation('variables'), $this->makeTransformer(ServerVariableTransformer::class), 'server_variable'); + return $this->collection($server->variables, new ServerVariableTransformer()); } /** * Return a generic array with location information for this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeLocation(Server $server): Item|NullResource { @@ -198,15 +169,11 @@ class ServerTransformer extends BaseTransformer return $this->null(); } - $server->loadMissing('location'); - - return $this->item($server->getRelation('location'), $this->makeTransformer(LocationTransformer::class), 'location'); + return $this->item($server->location, new LocationTransformer()); } /** * Return a generic array with node information for this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeNode(Server $server): Item|NullResource { @@ -214,15 +181,11 @@ class ServerTransformer extends BaseTransformer return $this->null(); } - $server->loadMissing('node'); - - return $this->item($server->getRelation('node'), $this->makeTransformer(NodeTransformer::class), 'node'); + return $this->item($server->node, new NodeTransformer()); } /** * Return a generic array with database information for this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeDatabases(Server $server): Collection|NullResource { @@ -230,8 +193,6 @@ class ServerTransformer extends BaseTransformer return $this->null(); } - $server->loadMissing('databases'); - - return $this->collection($server->getRelation('databases'), $this->makeTransformer(ServerDatabaseTransformer::class), 'databases'); + return $this->collection($server->databases, new ServerDatabaseTransformer()); } } diff --git a/app/Transformers/Api/Application/ServerVariableTransformer.php b/app/Transformers/Api/Application/ServerVariableTransformer.php index e27d1e013..7c3d1de75 100644 --- a/app/Transformers/Api/Application/ServerVariableTransformer.php +++ b/app/Transformers/Api/Application/ServerVariableTransformer.php @@ -7,8 +7,9 @@ use Pterodactyl\Models\EggVariable; use Pterodactyl\Models\ServerVariable; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; -class ServerVariableTransformer extends BaseTransformer +class ServerVariableTransformer extends Transformer { /** * List of resources that can be included. @@ -26,15 +27,13 @@ class ServerVariableTransformer extends BaseTransformer /** * Return a generic transformed server variable array. */ - public function transform(EggVariable $variable): array + public function transform(EggVariable $model): array { - return $variable->toArray(); + return $model->toArray(); } /** * Return the parent service variable data. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeParent(EggVariable $variable): Item|NullResource { @@ -42,8 +41,7 @@ class ServerVariableTransformer extends BaseTransformer return $this->null(); } - $variable->loadMissing('variable'); - - return $this->item($variable->getRelation('variable'), $this->makeTransformer(EggVariableTransformer::class), 'variable'); + // TODO: what the fuck? + return $this->item($variable->variable, new EggVariableTransformer()); } } diff --git a/app/Transformers/Api/Application/SubuserTransformer.php b/app/Transformers/Api/Application/SubuserTransformer.php index 0a51d61d9..7b3ef7c0b 100644 --- a/app/Transformers/Api/Application/SubuserTransformer.php +++ b/app/Transformers/Api/Application/SubuserTransformer.php @@ -6,13 +6,14 @@ use Pterodactyl\Models\Subuser; use League\Fractal\Resource\Item; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; -class SubuserTransformer extends BaseTransformer +class SubuserTransformer extends Transformer { /** * List of resources that can be included. */ - protected array $availableIncludes = ['user', 'server']; + protected array $availableIncludes = ['server', 'user']; /** * Return the resource name for the JSONAPI output. @@ -25,38 +26,20 @@ class SubuserTransformer extends BaseTransformer /** * Return a transformed Subuser model that can be consumed by external services. */ - public function transform(Subuser $subuser): array + public function transform(Subuser $model): array { return [ - 'id' => $subuser->id, - 'user_id' => $subuser->user_id, - 'server_id' => $subuser->server_id, - 'permissions' => $subuser->permissions, - 'created_at' => $this->formatTimestamp($subuser->created_at), - 'updated_at' => $this->formatTimestamp($subuser->updated_at), + 'id' => $model->id, + 'user_id' => $model->user_id, + 'server_id' => $model->server_id, + 'permissions' => $model->permissions, + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), ]; } - /** - * Return a generic item of user for this subuser. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeUser(Subuser $subuser): Item|NullResource - { - if (!$this->authorize(AdminAcl::RESOURCE_USERS)) { - return $this->null(); - } - - $subuser->loadMissing('user'); - - return $this->item($subuser->getRelation('user'), $this->makeTransformer(UserTransformer::class), 'user'); - } - /** * Return a generic item of server for this subuser. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeServer(Subuser $subuser): Item|NullResource { @@ -64,8 +47,18 @@ class SubuserTransformer extends BaseTransformer return $this->null(); } - $subuser->loadMissing('server'); + return $this->item($subuser->server, new ServerTransformer()); + } - return $this->item($subuser->getRelation('server'), $this->makeTransformer(ServerTransformer::class), 'server'); + /** + * Return a generic item of user for this subuser. + */ + public function includeUser(Subuser $subuser): Item|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_USERS)) { + return $this->null(); + } + + return $this->item($subuser->user, new UserTransformer()); } } diff --git a/app/Transformers/Api/Application/UserTransformer.php b/app/Transformers/Api/Application/UserTransformer.php index 14e354b45..c50a9d8c6 100644 --- a/app/Transformers/Api/Application/UserTransformer.php +++ b/app/Transformers/Api/Application/UserTransformer.php @@ -6,8 +6,9 @@ use Pterodactyl\Models\User; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; -class UserTransformer extends BaseTransformer +class UserTransformer extends Transformer { /** * List of resources that can be included. @@ -25,28 +26,27 @@ class UserTransformer extends BaseTransformer /** * Return a transformed User model that can be consumed by external services. */ - public function transform(User $user): array + public function transform(User $model): array { return [ - 'id' => $user->id, - 'external_id' => $user->external_id, - 'uuid' => $user->uuid, - 'username' => $user->username, - 'email' => $user->email, - 'first_name' => $user->name_first, - 'last_name' => $user->name_last, - 'language' => $user->language, - 'root_admin' => (bool) $user->root_admin, - '2fa' => (bool) $user->use_totp, - 'created_at' => $this->formatTimestamp($user->created_at), - 'updated_at' => $this->formatTimestamp($user->updated_at), + 'id' => $model->id, + 'external_id' => $model->external_id, + 'uuid' => $model->uuid, + 'username' => $model->username, + 'email' => $model->email, + 'language' => $model->language, + 'root_admin' => (bool) $model->root_admin, + '2fa' => (bool) $model->use_totp, + 'avatar_url' => $model->avatarURL(), + 'admin_role_id' => $model->admin_role_id, + 'role_name' => $model->adminRoleName(), + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), ]; } /** * Return the servers associated with this user. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeServers(User $user): Collection|NullResource { @@ -54,8 +54,6 @@ class UserTransformer extends BaseTransformer return $this->null(); } - $user->loadMissing('servers'); - - return $this->collection($user->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server'); + return $this->collection($user->servers, new ServerTransformer()); } } diff --git a/app/Transformers/Api/Client/AccountTransformer.php b/app/Transformers/Api/Client/AccountTransformer.php index 1a14555d8..68651d8f3 100644 --- a/app/Transformers/Api/Client/AccountTransformer.php +++ b/app/Transformers/Api/Client/AccountTransformer.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\User; +use Pterodactyl\Transformers\Api\Transformer; -class AccountTransformer extends BaseClientTransformer +class AccountTransformer extends Transformer { /** * Return the resource name for the JSONAPI output. diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php index 57c8ac30e..dc0be9613 100644 --- a/app/Transformers/Api/Client/ActivityLogTransformer.php +++ b/app/Transformers/Api/Client/ActivityLogTransformer.php @@ -4,10 +4,13 @@ namespace Pterodactyl\Transformers\Api\Client; use Illuminate\Support\Str; use Pterodactyl\Models\User; +use League\Fractal\Resource\Item; use Pterodactyl\Models\ActivityLog; use Illuminate\Database\Eloquent\Model; +use League\Fractal\Resource\NullResource; +use Pterodactyl\Transformers\Api\Transformer; -class ActivityLogTransformer extends BaseClientTransformer +class ActivityLogTransformer extends Transformer { protected array $availableIncludes = ['actor']; @@ -34,13 +37,13 @@ class ActivityLogTransformer extends BaseClientTransformer ]; } - public function includeActor(ActivityLog $model) + public function includeActor(ActivityLog $model): Item|NullResource { if (!$model->actor instanceof User) { return $this->null(); } - return $this->item($model->actor, $this->makeTransformer(UserTransformer::class), User::RESOURCE_NAME); + return $this->item($model->actor, new UserTransformer()); } /** diff --git a/app/Transformers/Api/Client/AllocationTransformer.php b/app/Transformers/Api/Client/AllocationTransformer.php index 2e63e2bc9..f39f2e397 100644 --- a/app/Transformers/Api/Client/AllocationTransformer.php +++ b/app/Transformers/Api/Client/AllocationTransformer.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\Allocation; +use Pterodactyl\Transformers\Api\Transformer; -class AllocationTransformer extends BaseClientTransformer +class AllocationTransformer extends Transformer { /** * Return the resource name for the JSONAPI output. diff --git a/app/Transformers/Api/Client/ApiKeyTransformer.php b/app/Transformers/Api/Client/ApiKeyTransformer.php index 92ee1a5c6..022b83ffc 100644 --- a/app/Transformers/Api/Client/ApiKeyTransformer.php +++ b/app/Transformers/Api/Client/ApiKeyTransformer.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\ApiKey; +use Pterodactyl\Transformers\Api\Transformer; -class ApiKeyTransformer extends BaseClientTransformer +class ApiKeyTransformer extends Transformer { /** * {@inheritdoc} diff --git a/app/Transformers/Api/Client/BackupTransformer.php b/app/Transformers/Api/Client/BackupTransformer.php index 298c99642..6797c40dd 100644 --- a/app/Transformers/Api/Client/BackupTransformer.php +++ b/app/Transformers/Api/Client/BackupTransformer.php @@ -3,26 +3,27 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\Backup; +use Pterodactyl\Transformers\Api\Transformer; -class BackupTransformer extends BaseClientTransformer +class BackupTransformer extends Transformer { public function getResourceName(): string { return Backup::RESOURCE_NAME; } - public function transform(Backup $backup): array + public function transform(Backup $model): array { return [ - 'uuid' => $backup->uuid, - 'is_successful' => $backup->is_successful, - 'is_locked' => $backup->is_locked, - 'name' => $backup->name, - 'ignored_files' => $backup->ignored_files, - 'checksum' => $backup->checksum, - 'bytes' => $backup->bytes, - 'created_at' => $backup->created_at->toAtomString(), - 'completed_at' => $backup->completed_at ? $backup->completed_at->toAtomString() : null, + 'uuid' => $model->uuid, + 'is_successful' => $model->is_successful, + 'is_locked' => $model->is_locked, + 'name' => $model->name, + 'ignored_files' => $model->ignored_files, + 'checksum' => $model->checksum, + 'bytes' => $model->bytes, + 'created_at' => self::formatTimestamp($model->created_at), + 'completed_at' => self::formatTimestamp($model->completed_at), ]; } } diff --git a/app/Transformers/Api/Client/BaseClientTransformer.php b/app/Transformers/Api/Client/BaseClientTransformer.php deleted file mode 100644 index 0388effb0..000000000 --- a/app/Transformers/Api/Client/BaseClientTransformer.php +++ /dev/null @@ -1,43 +0,0 @@ -request->user(); - } - - /** - * Determine if the API key loaded onto the transformer has permission - * to access a different resource. This is used when including other - * models on a transformation request. - * - * @noinspection PhpParameterNameChangedDuringInheritanceInspection - */ - protected function authorize(string $ability, Server $server = null): bool - { - Assert::isInstanceOf($server, Server::class); - - return $this->request->user()->can($ability, [$server]); - } - - /** - * {@inheritDoc} - */ - protected function makeTransformer(string $abstract) - { - Assert::subclassOf($abstract, self::class); - - return parent::makeTransformer($abstract); - } -} diff --git a/app/Transformers/Api/Client/DatabaseTransformer.php b/app/Transformers/Api/Client/DatabaseTransformer.php index 23e966637..5ad899ad9 100644 --- a/app/Transformers/Api/Client/DatabaseTransformer.php +++ b/app/Transformers/Api/Client/DatabaseTransformer.php @@ -6,15 +6,15 @@ use Pterodactyl\Models\Database; use League\Fractal\Resource\Item; use Pterodactyl\Models\Permission; use League\Fractal\Resource\NullResource; +use Pterodactyl\Transformers\Api\Transformer; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Extensions\HashidsInterface; -class DatabaseTransformer extends BaseClientTransformer +class DatabaseTransformer extends Transformer { protected array $availableIncludes = ['password']; private Encrypter $encrypter; - private HashidsInterface $hashids; /** @@ -38,8 +38,8 @@ class DatabaseTransformer extends BaseClientTransformer return [ 'id' => $this->hashids->encode($model->id), 'host' => [ - 'address' => $model->getRelation('host')->host, - 'port' => $model->getRelation('host')->port, + 'address' => $model->host->host, + 'port' => $model->host->port, ], 'name' => $model->database, 'username' => $model->username, @@ -53,7 +53,7 @@ class DatabaseTransformer extends BaseClientTransformer */ public function includePassword(Database $database): Item|NullResource { - if (!$this->request->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $database->server)) { + if ($this->user()->cannot(Permission::ACTION_DATABASE_VIEW_PASSWORD, $database->server)) { return $this->null(); } diff --git a/app/Transformers/Api/Client/EggTransformer.php b/app/Transformers/Api/Client/EggTransformer.php index 8e2e3474e..493958da5 100644 --- a/app/Transformers/Api/Client/EggTransformer.php +++ b/app/Transformers/Api/Client/EggTransformer.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\Egg; +use Pterodactyl\Transformers\Api\Transformer; -class EggTransformer extends BaseClientTransformer +class EggTransformer extends Transformer { /** * Return the resource name for the JSONAPI output. @@ -14,11 +15,11 @@ class EggTransformer extends BaseClientTransformer return Egg::RESOURCE_NAME; } - public function transform(Egg $egg): array + public function transform(Egg $model): array { return [ - 'uuid' => $egg->uuid, - 'name' => $egg->name, + 'uuid' => $model->uuid, + 'name' => $model->name, ]; } } diff --git a/app/Transformers/Api/Client/EggVariableTransformer.php b/app/Transformers/Api/Client/EggVariableTransformer.php index 09c344249..ea0ead081 100644 --- a/app/Transformers/Api/Client/EggVariableTransformer.php +++ b/app/Transformers/Api/Client/EggVariableTransformer.php @@ -3,31 +3,32 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\EggVariable; +use Pterodactyl\Transformers\Api\Transformer; -class EggVariableTransformer extends BaseClientTransformer +class EggVariableTransformer extends Transformer { public function getResourceName(): string { return EggVariable::RESOURCE_NAME; } - public function transform(EggVariable $variable): array + public function transform(EggVariable $model): array { // This guards against someone incorrectly retrieving variables (haha, me) and then passing // them into the transformer and along to the user. Just throw an exception and break the entire // pathway since you should never be exposing these types of variables to a client. - if (!$variable->user_viewable) { + if (!$model->user_viewable) { throw new \BadMethodCallException('Cannot transform a hidden egg variable in a client transformer.'); } return [ - 'name' => $variable->name, - 'description' => $variable->description, - 'env_variable' => $variable->env_variable, - 'default_value' => $variable->default_value, - 'server_value' => $variable->server_value, - 'is_editable' => $variable->user_editable, - 'rules' => $variable->rules, + 'name' => $model->name, + 'description' => $model->description, + 'env_variable' => $model->env_variable, + 'default_value' => $model->default_value, + 'server_value' => $model->server_value, + 'is_editable' => $model->user_editable, + 'rules' => $model->rules, ]; } } diff --git a/app/Transformers/Api/Client/FileObjectTransformer.php b/app/Transformers/Api/Client/FileObjectTransformer.php index 6278dad73..94a966000 100644 --- a/app/Transformers/Api/Client/FileObjectTransformer.php +++ b/app/Transformers/Api/Client/FileObjectTransformer.php @@ -4,29 +4,30 @@ namespace Pterodactyl\Transformers\Api\Client; use Carbon\Carbon; use Illuminate\Support\Arr; +use Pterodactyl\Transformers\Api\Transformer; -class FileObjectTransformer extends BaseClientTransformer +class FileObjectTransformer extends Transformer { - /** - * Transform a file object response from the daemon into a standardized response. - */ - public function transform(array $item): array - { - return [ - 'name' => Arr::get($item, 'name'), - 'mode' => Arr::get($item, 'mode'), - 'mode_bits' => Arr::get($item, 'mode_bits'), - 'size' => Arr::get($item, 'size'), - 'is_file' => Arr::get($item, 'file', true), - 'is_symlink' => Arr::get($item, 'symlink', false), - 'mimetype' => Arr::get($item, 'mime', 'application/octet-stream'), - 'created_at' => Carbon::parse(Arr::get($item, 'created', ''))->toAtomString(), - 'modified_at' => Carbon::parse(Arr::get($item, 'modified', ''))->toAtomString(), - ]; - } - public function getResourceName(): string { return 'file_object'; } + + /** + * Transform a file object response from the daemon into a standardized response. + */ + public function transform(array $model): array + { + return [ + 'name' => Arr::get($model, 'name'), + 'mode' => Arr::get($model, 'mode'), + 'mode_bits' => Arr::get($model, 'mode_bits'), + 'size' => Arr::get($model, 'size'), + 'is_file' => Arr::get($model, 'file', true), + 'is_symlink' => Arr::get($model, 'symlink', false), + 'mimetype' => Arr::get($model, 'mime', 'application/octet-stream'), + 'created_at' => Carbon::parse(Arr::get($model, 'created', ''))->toAtomString(), + 'modified_at' => Carbon::parse(Arr::get($model, 'modified', ''))->toAtomString(), + ]; + } } diff --git a/app/Transformers/Api/Client/ScheduleTransformer.php b/app/Transformers/Api/Client/ScheduleTransformer.php index 98c783f45..735e8fdd8 100644 --- a/app/Transformers/Api/Client/ScheduleTransformer.php +++ b/app/Transformers/Api/Client/ScheduleTransformer.php @@ -2,11 +2,11 @@ namespace Pterodactyl\Transformers\Api\Client; -use Pterodactyl\Models\Task; use Pterodactyl\Models\Schedule; use League\Fractal\Resource\Collection; +use Pterodactyl\Transformers\Api\Transformer; -class ScheduleTransformer extends BaseClientTransformer +class ScheduleTransformer extends Transformer { protected array $availableIncludes = ['tasks']; @@ -38,24 +38,18 @@ class ScheduleTransformer extends BaseClientTransformer 'is_active' => $model->is_active, 'is_processing' => $model->is_processing, 'only_when_online' => $model->only_when_online, - 'last_run_at' => $model->last_run_at?->toAtomString(), - 'next_run_at' => $model->next_run_at?->toAtomString(), - 'created_at' => $model->created_at->toAtomString(), - 'updated_at' => $model->updated_at->toAtomString(), + 'last_run_at' => self::formatTimestamp($model->last_run_at), + 'next_run_at' => self::formatTimestamp($model->next_run_at), + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), ]; } /** * Allows attaching the tasks specific to the schedule in the response. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeTasks(Schedule $model): Collection { - return $this->collection( - $model->tasks, - $this->makeTransformer(TaskTransformer::class), - Task::RESOURCE_NAME - ); + return $this->collection($model->tasks, new TaskTransformer()); } } diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 9f7bce958..782e6b435 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -12,9 +12,10 @@ use Illuminate\Container\Container; use Pterodactyl\Models\EggVariable; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; +use Pterodactyl\Transformers\Api\Transformer; use Pterodactyl\Services\Servers\StartupCommandService; -class ServerTransformer extends BaseClientTransformer +class ServerTransformer extends Transformer { protected array $defaultIncludes = ['allocations', 'variables']; @@ -67,22 +68,16 @@ class ServerTransformer extends BaseClientTransformer 'backups' => $server->backup_limit, ], 'status' => $server->status, - // This field is deprecated, please use "status". - 'is_suspended' => $server->isSuspended(), - // This field is deprecated, please use "status". - 'is_installing' => !$server->isInstalled(), 'is_transferring' => !is_null($server->transfer), ]; } /** * Returns the allocations associated with this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeAllocations(Server $server): Collection { - $transformer = $this->makeTransformer(AllocationTransformer::class); + $transformer = new AllocationTransformer(); $user = $this->request->user(); // While we include this permission, we do need to actually handle it slightly different here @@ -96,42 +91,31 @@ class ServerTransformer extends BaseClientTransformer $primary = clone $server->allocation; $primary->notes = null; - return $this->collection([$primary], $transformer, Allocation::RESOURCE_NAME); + return $this->collection([$primary], $transformer); } - return $this->collection($server->allocations, $transformer, Allocation::RESOURCE_NAME); + return $this->collection($server->allocations, $transformer); } - /** - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException - */ public function includeVariables(Server $server): Collection|NullResource { if (!$this->request->user()->can(Permission::ACTION_STARTUP_READ, $server)) { return $this->null(); } - return $this->collection( - $server->variables->where('user_viewable', true), - $this->makeTransformer(EggVariableTransformer::class), - EggVariable::RESOURCE_NAME - ); + return $this->collection($server->variables->where('user_viewable', true), new EggVariableTransformer()); } /** * Returns the egg associated with this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeEgg(Server $server): Item { - return $this->item($server->egg, $this->makeTransformer(EggTransformer::class), Egg::RESOURCE_NAME); + return $this->item($server->egg, new EggTransformer()); } /** * Returns the subusers associated with this server. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeSubusers(Server $server): Collection|NullResource { @@ -139,6 +123,6 @@ class ServerTransformer extends BaseClientTransformer return $this->null(); } - return $this->collection($server->subusers, $this->makeTransformer(SubuserTransformer::class), Subuser::RESOURCE_NAME); + return $this->collection($server->subusers, new SubuserTransformer()); } } diff --git a/app/Transformers/Api/Client/StatsTransformer.php b/app/Transformers/Api/Client/StatsTransformer.php index 6b323b315..0ae696cc3 100644 --- a/app/Transformers/Api/Client/StatsTransformer.php +++ b/app/Transformers/Api/Client/StatsTransformer.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Illuminate\Support\Arr; +use Pterodactyl\Transformers\Api\Transformer; -class StatsTransformer extends BaseClientTransformer +class StatsTransformer extends Transformer { public function getResourceName(): string { @@ -15,18 +16,18 @@ class StatsTransformer extends BaseClientTransformer * Transform stats from the daemon into a result set that can be used in * the client API. */ - public function transform(array $data): array + public function transform(array $model): array { return [ - 'current_state' => Arr::get($data, 'state', 'stopped'), - 'is_suspended' => Arr::get($data, 'is_suspended', false), + 'current_state' => Arr::get($model, 'state', 'stopped'), + 'is_suspended' => Arr::get($model, 'is_suspended', false), 'resources' => [ - 'memory_bytes' => Arr::get($data, 'utilization.memory_bytes', 0), - 'cpu_absolute' => Arr::get($data, 'utilization.cpu_absolute', 0), - 'disk_bytes' => Arr::get($data, 'utilization.disk_bytes', 0), - 'network_rx_bytes' => Arr::get($data, 'utilization.network.rx_bytes', 0), - 'network_tx_bytes' => Arr::get($data, 'utilization.network.tx_bytes', 0), - 'uptime' => Arr::get($data, 'utilization.uptime', 0), + 'memory_bytes' => Arr::get($model, 'utilization.memory_bytes', 0), + 'cpu_absolute' => Arr::get($model, 'utilization.cpu_absolute', 0), + 'disk_bytes' => Arr::get($model, 'utilization.disk_bytes', 0), + 'network_rx_bytes' => Arr::get($model, 'utilization.network.rx_bytes', 0), + 'network_tx_bytes' => Arr::get($model, 'utilization.network.tx_bytes', 0), + 'uptime' => Arr::get($model, 'utilization.uptime', 0), ], ]; } diff --git a/app/Transformers/Api/Client/SubuserTransformer.php b/app/Transformers/Api/Client/SubuserTransformer.php index 2902d1f7a..4d26ae655 100644 --- a/app/Transformers/Api/Client/SubuserTransformer.php +++ b/app/Transformers/Api/Client/SubuserTransformer.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\Subuser; +use Pterodactyl\Transformers\Api\Transformer; -class SubuserTransformer extends BaseClientTransformer +class SubuserTransformer extends Transformer { /** * Return the resource name for the JSONAPI output. @@ -16,13 +17,11 @@ class SubuserTransformer extends BaseClientTransformer /** * Transforms a subuser into a model that can be shown to a front-end user. - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function transform(Subuser $model): array { return array_merge( - $this->makeTransformer(UserTransformer::class)->transform($model->user), + (new UserTransformer())->transform($model->user), ['permissions' => $model->permissions] ); } diff --git a/app/Transformers/Api/Client/TaskTransformer.php b/app/Transformers/Api/Client/TaskTransformer.php index 52f0e2b5d..84054651e 100644 --- a/app/Transformers/Api/Client/TaskTransformer.php +++ b/app/Transformers/Api/Client/TaskTransformer.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\Task; +use Pterodactyl\Transformers\Api\Transformer; -class TaskTransformer extends BaseClientTransformer +class TaskTransformer extends Transformer { /** * {@inheritdoc} @@ -27,8 +28,8 @@ class TaskTransformer extends BaseClientTransformer 'time_offset' => $model->time_offset, 'is_queued' => $model->is_queued, 'continue_on_failure' => $model->continue_on_failure, - 'created_at' => $model->created_at->toAtomString(), - 'updated_at' => $model->updated_at->toAtomString(), + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), ]; } } diff --git a/app/Transformers/Api/Client/UserSSHKeyTransformer.php b/app/Transformers/Api/Client/UserSSHKeyTransformer.php index 015a017b6..a13370eb5 100644 --- a/app/Transformers/Api/Client/UserSSHKeyTransformer.php +++ b/app/Transformers/Api/Client/UserSSHKeyTransformer.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\UserSSHKey; +use Pterodactyl\Transformers\Api\Transformer; -class UserSSHKeyTransformer extends BaseClientTransformer +class UserSSHKeyTransformer extends Transformer { public function getResourceName(): string { @@ -20,7 +21,7 @@ class UserSSHKeyTransformer extends BaseClientTransformer 'name' => $model->name, 'fingerprint' => $model->fingerprint, 'public_key' => $model->public_key, - 'created_at' => $model->created_at->toAtomString(), + 'created_at' => self::formatTimestamp($model->created_at), ]; } } diff --git a/app/Transformers/Api/Client/UserTransformer.php b/app/Transformers/Api/Client/UserTransformer.php index 04bc70ea6..b6275c3c1 100644 --- a/app/Transformers/Api/Client/UserTransformer.php +++ b/app/Transformers/Api/Client/UserTransformer.php @@ -4,8 +4,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Illuminate\Support\Str; use Pterodactyl\Models\User; +use Pterodactyl\Transformers\Api\Transformer; -class UserTransformer extends BaseClientTransformer +class UserTransformer extends Transformer { /** * Return the resource name for the JSONAPI output. @@ -25,9 +26,9 @@ class UserTransformer extends BaseClientTransformer 'uuid' => $model->uuid, 'username' => $model->username, 'email' => $model->email, - 'image' => 'https://gravatar.com/avatar/' . md5(Str::lower($model->email)), + 'image' => $model->avatarURL(), '2fa_enabled' => $model->use_totp, - 'created_at' => $model->created_at->toAtomString(), + 'created_at' => self::formatTimestamp($model->created_at), ]; } } diff --git a/app/Transformers/Api/Transformer.php b/app/Transformers/Api/Transformer.php new file mode 100644 index 000000000..8b7e013bb --- /dev/null +++ b/app/Transformers/Api/Transformer.php @@ -0,0 +1,156 @@ +request = Container::getInstance()->make('request'); + + if (method_exists($this, 'handle')) { + Container::getInstance()->call([$this, 'handle']); + } + } + + /** + * Returns the resource name for the transformed item. + */ + abstract public function getResourceName(): string; + + /** + * Returns the authorized user for the request. + */ + protected function user(): User + { + return $this->request->user(); + } + + /** + * Determines if the user making this request is authorized to access the given + * resource on the API. This is used when requested included items to ensure that + * the user and key are authorized to see the result. + * + * TODO: implement this with the new API key formats. + */ + protected function authorize(string $resource): bool + { + return $this->request->user() instanceof User; + } + + /** + * {@inheritDoc} + * + * @param mixed $data + * @param callable|\League\Fractal\TransformerAbstract $transformer + */ + protected function item($data, $transformer, ?string $resourceKey = null): Item + { + if (!$transformer instanceof Closure) { + self::assertSameNamespace($transformer); + } + + $item = parent::item($data, $transformer, $resourceKey); + + if (!$item->getResourceKey() && method_exists($transformer, 'getResourceName')) { + $item->setResourceKey($transformer->getResourceName()); + } + + return $item; + } + + /** + * {@inheritDoc} + * + * @param mixed $data + * @param callable|\League\Fractal\TransformerAbstract $transformer + */ + protected function collection($data, $transformer, ?string $resourceKey = null): Collection + { + if (!$transformer instanceof Closure) { + self::assertSameNamespace($transformer); + } + + $collection = parent::collection($data, $transformer, $resourceKey); + + if (!$collection->getResourceKey() && method_exists($transformer, 'getResourceName')) { + $collection->setResourceKey($transformer->getResourceName()); + } + + return $collection; + } + + /** + * Sets the default timezone to use for transformed responses. Pass a null value + * to return back to the default timezone (UTC). + */ + public static function setTimezone(string $tz = null) + { + static::$timezone = $tz ?? 'UTC'; + } + + /** + * Asserts that the given transformer is the same base namespace as the class that + * implements this abstract transformer class. This prevents a client or application + * transformer from unintentionally transforming a resource using an unexpected type. + * + * @param callable|\League\Fractal\TransformerAbstract $transformer + */ + protected static function assertSameNamespace($transformer) + { + Assert::subclassOf($transformer, TransformerAbstract::class); + + $namespace = substr(get_class($transformer), 0, strlen(class_basename($transformer)) * -1); + $expected = substr(static::class, 0, strlen(class_basename(static::class)) * -1); + + Assert::same($namespace, $expected, 'Cannot invoke a new transformer (%s) that is not in the same namespace (%s).'); + } + + /** + * Returns an ISO-8601 formatted timestamp to use in API responses. This + * time is returned in the default transformer timezone if no timezone value + * is provided. + * + * If no time is provided a null value is returned. + * + * @param string|\DateTimeInterface|null $timestamp + */ + protected static function formatTimestamp($timestamp, string $tz = null): ?string + { + if (empty($timestamp)) { + return null; + } + + if ($timestamp instanceof DateTimeInterface) { + $value = CarbonImmutable::instance($timestamp); + } else { + $value = CarbonImmutable::createFromFormat(CarbonInterface::DEFAULT_TO_STRING_FORMAT, $timestamp); + } + + return $value->setTimezone($tz ?? self::$timezone)->toAtomString(); + } +} diff --git a/database/Seeders/EggSeeder.php b/database/Seeders/EggSeeder.php index 234e7b5a4..523494714 100644 --- a/database/Seeders/EggSeeder.php +++ b/database/Seeders/EggSeeder.php @@ -11,10 +11,6 @@ use Pterodactyl\Services\Eggs\Sharing\EggUpdateImporterService; class EggSeeder extends Seeder { - protected EggImporterService $importerService; - - protected EggUpdateImporterService $updateImporterService; - /** * @var string[] */ @@ -29,15 +25,15 @@ class EggSeeder extends Seeder * EggSeeder constructor. */ public function __construct( - EggImporterService $importerService, - EggUpdateImporterService $updateImporterService + private EggImporterService $importerService, + private EggUpdateImporterService $updateImporterService ) { - $this->importerService = $importerService; - $this->updateImporterService = $updateImporterService; } /** * Run the egg seeder. + * + * @throws \JsonException */ public function run() { @@ -51,6 +47,8 @@ class EggSeeder extends Seeder /** * Loop through the list of egg files and import them. + * + * @throws \JsonException */ protected function parseEggFiles(Nest $nest) { @@ -75,7 +73,7 @@ class EggSeeder extends Seeder $this->updateImporterService->handle($egg, $file); $this->command->info('Updated ' . $decoded['name']); } else { - $this->importerService->handle($file, $nest->id); + $this->importerService->handleFile($nest->id, $file); $this->command->comment('Created ' . $decoded['name']); } } diff --git a/database/migrations/2020_09_25_021109_create_admin_roles_table.php b/database/migrations/2020_09_25_021109_create_admin_roles_table.php new file mode 100644 index 000000000..e67f75074 --- /dev/null +++ b/database/migrations/2020_09_25_021109_create_admin_roles_table.php @@ -0,0 +1,31 @@ +increments('id'); + $table->string('name', 64); + $table->string('description', 255)->nullable(); + $table->integer('sort_id'); + $table->json('permissions')->nullable(); + + $table->unique(['id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('admin_roles'); + } +}; diff --git a/database/migrations/2021_01_16_201057_add_admin_role_id_column_to_users_table.php b/database/migrations/2021_01_16_201057_add_admin_role_id_column_to_users_table.php new file mode 100644 index 000000000..641dc62db --- /dev/null +++ b/database/migrations/2021_01_16_201057_add_admin_role_id_column_to_users_table.php @@ -0,0 +1,30 @@ +integer('admin_role_id')->nullable()->unsigned()->after('language'); + $table->index('admin_role_id'); + $table->foreign('admin_role_id')->references('id')->on('admin_roles')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['admin_role_id']); + $table->dropColumn('admin_role_id'); + }); + } +}; diff --git a/database/migrations/2021_01_30_193343_add_database_host_id_column_to_nodes_table.php b/database/migrations/2021_01_30_193343_add_database_host_id_column_to_nodes_table.php new file mode 100644 index 000000000..e45ad7618 --- /dev/null +++ b/database/migrations/2021_01_30_193343_add_database_host_id_column_to_nodes_table.php @@ -0,0 +1,41 @@ +dropForeign(['node_id']); + $table->dropColumn('node_id'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->integer('database_host_id')->nullable()->unsigned()->after('location_id'); + $table->index('database_host_id')->nullable(); + $table->foreign('database_host_id')->references('id')->on('database_hosts')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('nodes', function (Blueprint $table) { + $table->dropForeign(['database_host_id']); + $table->dropColumn('database_host_id'); + }); + + Schema::table('database_hosts', function (Blueprint $table) { + $table->integer('node_id')->nullable()->unsigned()->after('max_databases'); + $table->index('node_id')->nullable(); + $table->foreign('node_id')->references('id')->on('nodes'); + }); + } +}; diff --git a/database/migrations/2021_02_23_212658_change_port_columns_on_nodes_table.php b/database/migrations/2021_02_23_212658_change_port_columns_on_nodes_table.php new file mode 100644 index 000000000..2e8010c9b --- /dev/null +++ b/database/migrations/2021_02_23_212658_change_port_columns_on_nodes_table.php @@ -0,0 +1,58 @@ +renameColumn('daemonListen', 'listen_port_http'); + $table->renameColumn('daemonSFTP', 'listen_port_sftp'); + $table->renameColumn('daemonBase', 'daemon_base'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->integer('listen_port_http')->unsigned()->default(8080)->after('fqdn')->change(); + $table->integer('listen_port_sftp')->unsigned()->default(2022)->after('listen_port_sftp')->change(); + + $table->integer('public_port_http')->unsigned()->default(8080)->after('listen_port_http'); + $table->integer('public_port_sftp')->unsigned()->default(2022)->after('listen_port_sftp'); + }); + + DB::transaction(function () { + foreach (DB::select('SELECT id, listen_port_http, listen_port_sftp FROM nodes') as $datum) { + DB::update('UPDATE nodes SET public_port_http = ?, public_port_sftp = ? WHERE id = ?', [ + $datum->listen_port_http, + $datum->listen_port_sftp, + $datum->id, + ]); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('nodes', function (Blueprint $table) { + $table->renameColumn('listen_port_http', 'daemonListen'); + $table->renameColumn('listen_port_sftp', 'daemonSFTP'); + $table->renameColumn('daemon_base', 'daemonBase'); + + $table->dropColumn('public_port_http'); + $table->dropColumn('public_port_sftp'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->smallInteger('daemonListen')->unsigned()->default(8080)->after('daemon_token')->change(); + $table->smallInteger('daemonSFTP')->unsigned()->default(2022)->after('daemonListen')->change(); + }); + } +}; diff --git a/database/migrations/2021_07_29_032255_yeet_names_from_users_table.php b/database/migrations/2021_07_29_032255_yeet_names_from_users_table.php new file mode 100644 index 000000000..0a22a2059 --- /dev/null +++ b/database/migrations/2021_07_29_032255_yeet_names_from_users_table.php @@ -0,0 +1,28 @@ +dropColumn(['name_first', 'name_last']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('name_first')->after('email')->nullable(); + $table->string('name_last')->after('name_first')->nullable(); + }); + } +}; diff --git a/database/migrations/2021_10_23_185304_drop_config_logs_column_from_eggs_table.php b/database/migrations/2021_10_23_185304_drop_config_logs_column_from_eggs_table.php new file mode 100644 index 000000000..b78a9698c --- /dev/null +++ b/database/migrations/2021_10_23_185304_drop_config_logs_column_from_eggs_table.php @@ -0,0 +1,27 @@ +dropColumn('config_logs'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('eggs', function (Blueprint $table) { + $table->text('config_logs')->nullable()->after('docker_image'); + }); + } +}; diff --git a/database/migrations/2021_10_23_202643_update_default_values_for_eggs.php b/database/migrations/2021_10_23_202643_update_default_values_for_eggs.php new file mode 100644 index 000000000..fee18b92b --- /dev/null +++ b/database/migrations/2021_10_23_202643_update_default_values_for_eggs.php @@ -0,0 +1,28 @@ +string('script_container')->default('ghcr.io/pterodactyl/installers:alpine')->after('startup')->change(); + $table->string('script_entry')->default('/bin/ash')->after('copy_script_from')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('eggs', function (Blueprint $table) { + // You are stuck with the new values because I am too lazy to revert them :) + }); + } +}; diff --git a/database/migrations/2021_11_01_180130_make_startup_field_nullable_on_servers_table.php b/database/migrations/2021_11_01_180130_make_startup_field_nullable_on_servers_table.php new file mode 100644 index 000000000..3795eb752 --- /dev/null +++ b/database/migrations/2021_11_01_180130_make_startup_field_nullable_on_servers_table.php @@ -0,0 +1,27 @@ +text('startup')->default(null)->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->text('startup')->change(); + }); + } +}; diff --git a/resources/views/templates/wrapper.blade.php b/resources/views/templates/wrapper.blade.php index 8624d2e85..dd3ed4c5d 100644 --- a/resources/views/templates/wrapper.blade.php +++ b/resources/views/templates/wrapper.blade.php @@ -22,7 +22,7 @@ @section('user-data') @if(!is_null(Auth::user())) @endif @if(!empty($siteConfiguration)) diff --git a/routes/api-application.php b/routes/api-application.php index dc6b0e5bb..cedbcd1d0 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -3,50 +3,47 @@ use Illuminate\Support\Facades\Route; use Pterodactyl\Http\Controllers\Api\Application; +Route::get('/version', [Application\VersionController::class]); + /* |-------------------------------------------------------------------------- -| User Controller Routes +| Database Controller Routes |-------------------------------------------------------------------------- | -| Endpoint: /api/application/users +| Endpoint: /api/application/databases | */ +Route::group(['prefix' => '/databases'], function () { + Route::get('/', [Application\Databases\DatabaseController::class, 'index']); + Route::get('/{databaseHost}', [Application\Databases\DatabaseController::class, 'view']); -Route::group(['prefix' => '/users'], function () { - Route::get('/', [Application\Users\UserController::class, 'index'])->name('api.application.users'); - Route::get('/{user:id}', [Application\Users\UserController::class, 'view'])->name('api.application.users.view'); - Route::get('/external/{external_id}', [Application\Users\ExternalUserController::class, 'index'])->name('api.application.users.external'); + Route::post('/', [Application\Databases\DatabaseController::class, 'store']); - Route::post('/', [Application\Users\UserController::class, 'store']); - Route::patch('/{user:id}', [Application\Users\UserController::class, 'update']); + Route::patch('/{databaseHost}', [Application\Databases\DatabaseController::class, 'update']); - Route::delete('/{user:id}', [Application\Users\UserController::class, 'delete']); + Route::delete('/{databaseHost}', [Application\Databases\DatabaseController::class, 'delete']); }); /* |-------------------------------------------------------------------------- -| Node Controller Routes +| Egg Controller Routes |-------------------------------------------------------------------------- | -| Endpoint: /api/application/nodes +| Endpoint: /api/application/eggs | */ -Route::group(['prefix' => '/nodes'], function () { - Route::get('/', [Application\Nodes\NodeController::class, 'index'])->name('api.application.nodes'); - Route::get('/deployable', Application\Nodes\NodeDeploymentController::class); - Route::get('/{node:id}', [Application\Nodes\NodeController::class, 'view'])->name('api.application.nodes.view'); - Route::get('/{node:id}/configuration', Application\Nodes\NodeConfigurationController::class); +Route::group(['prefix' => '/eggs'], function () { + Route::get('/{egg}', [Application\Eggs\EggController::class, 'view']); + Route::get('/{egg}/export', [Application\Eggs\EggController::class, 'export']); - Route::post('/', [Application\Nodes\NodeController::class, 'store']); - Route::patch('/{node:id}', [Application\Nodes\NodeController::class, 'update']); + Route::post('/', [Application\Eggs\EggController::class, 'store']); + Route::post('/{egg}/variables', [Application\Eggs\EggVariableController::class, 'store']); - Route::delete('/{node:id}', [Application\Nodes\NodeController::class, 'delete']); + Route::patch('/{egg}', [Application\Eggs\EggController::class, 'update']); + Route::patch('/{egg}/variables', [Application\Eggs\EggVariableController::class, 'update']); - Route::group(['prefix' => '/{node:id}/allocations'], function () { - Route::get('/', [Application\Nodes\AllocationController::class, 'index'])->name('api.application.allocations'); - Route::post('/', [Application\Nodes\AllocationController::class, 'store']); - Route::delete('/{allocation:id}', [Application\Nodes\AllocationController::class, 'delete'])->name('api.application.allocations.view'); - }); + Route::delete('/{egg}', [Application\Eggs\EggController::class, 'delete']); + Route::delete('/{egg}/variables/{eggVariable}', [Application\Eggs\EggVariableController::class, 'delete']); }); /* @@ -58,50 +55,38 @@ Route::group(['prefix' => '/nodes'], function () { | */ Route::group(['prefix' => '/locations'], function () { - Route::get('/', [Application\Locations\LocationController::class, 'index'])->name('api.applications.locations'); - Route::get('/{location:id}', [Application\Locations\LocationController::class, 'view'])->name('api.application.locations.view'); + Route::get('/', [Application\Locations\LocationController::class, 'index']); + Route::get('/{location}', [Application\Locations\LocationController::class, 'view']); Route::post('/', [Application\Locations\LocationController::class, 'store']); - Route::patch('/{location:id}', [Application\Locations\LocationController::class, 'update']); - Route::delete('/{location:id}', [Application\Locations\LocationController::class, 'delete']); + Route::patch('/{location}', [Application\Locations\LocationController::class, 'update']); + + Route::delete('/{location}', [Application\Locations\LocationController::class, 'delete']); }); /* |-------------------------------------------------------------------------- -| Server Controller Routes +| Mount Controller Routes |-------------------------------------------------------------------------- | -| Endpoint: /api/application/servers +| Endpoint: /api/application/mounts | */ -Route::group(['prefix' => '/servers'], function () { - Route::get('/', [Application\Servers\ServerController::class, 'index'])->name('api.application.servers'); - Route::get('/{server:id}', [Application\Servers\ServerController::class, 'view'])->name('api.application.servers.view'); - Route::get('/external/{external_id}', [Application\Servers\ExternalServerController::class, 'index'])->name('api.application.servers.external'); +Route::group(['prefix' => '/mounts'], function () { + Route::get('/', [Application\Mounts\MountController::class, 'index']); + Route::get('/{mount}', [Application\Mounts\MountController::class, 'view']); - Route::patch('/{server:id}/details', [Application\Servers\ServerDetailsController::class, 'details'])->name('api.application.servers.details'); - Route::patch('/{server:id}/build', [Application\Servers\ServerDetailsController::class, 'build'])->name('api.application.servers.build'); - Route::patch('/{server:id}/startup', [Application\Servers\StartupController::class, 'index'])->name('api.application.servers.startup'); + Route::post('/', [Application\Mounts\MountController::class, 'store']); - Route::post('/', [Application\Servers\ServerController::class, 'store']); - Route::post('/{server:id}/suspend', [Application\Servers\ServerManagementController::class, 'suspend'])->name('api.application.servers.suspend'); - Route::post('/{server:id}/unsuspend', [Application\Servers\ServerManagementController::class, 'unsuspend'])->name('api.application.servers.unsuspend'); - Route::post('/{server:id}/reinstall', [Application\Servers\ServerManagementController::class, 'reinstall'])->name('api.application.servers.reinstall'); + Route::put('/{mount}/eggs', [Application\Mounts\MountController::class, 'addEggs']); + Route::put('/{mount}/nodes', [Application\Mounts\MountController::class, 'addNodes']); - Route::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']); - Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']); + Route::patch('/{mount}', [Application\Mounts\MountController::class, 'update']); - // Database Management Endpoint - Route::group(['prefix' => '/{server:id}/databases'], function () { - Route::get('/', [Application\Servers\DatabaseController::class, 'index'])->name('api.application.servers.databases'); - Route::get('/{database:id}', [Application\Servers\DatabaseController::class, 'view'])->name('api.application.servers.databases.view'); - - Route::post('/', [Application\Servers\DatabaseController::class, 'store']); - Route::post('/{database:id}/reset-password', [Application\Servers\DatabaseController::class, 'resetPassword']); - - Route::delete('/{database:id}', [Application\Servers\DatabaseController::class, 'delete']); - }); + Route::delete('/{mount}', [Application\Mounts\MountController::class, 'delete']); + Route::delete('/{mount}/eggs', [Application\Mounts\MountController::class, 'deleteEggs']); + Route::delete('/{mount}/nodes', [Application\Mounts\MountController::class, 'deleteNodes']); }); /* @@ -113,12 +98,117 @@ Route::group(['prefix' => '/servers'], function () { | */ Route::group(['prefix' => '/nests'], function () { - Route::get('/', [Application\Nests\NestController::class, 'index'])->name('api.application.nests'); - Route::get('/{nest:id}', [Application\Nests\NestController::class, 'view'])->name('api.application.nests.view'); + Route::get('/', [Application\Nests\NestController::class, 'index']); + Route::get('/{nest}', [Application\Nests\NestController::class, 'view']); + Route::get('/{nest}/eggs', [Application\Eggs\EggController::class, 'index']); - // Egg Management Endpoint - Route::group(['prefix' => '/{nest:id}/eggs'], function () { - Route::get('/', [Application\Nests\EggController::class, 'index'])->name('api.application.nests.eggs'); - Route::get('/{egg:id}', [Application\Nests\EggController::class, 'view'])->name('api.application.nests.eggs.view'); + Route::post('/', [Application\Nests\NestController::class, 'store']); + Route::post('/{nest}/import', [Application\Nests\NestController::class, 'import']); + + Route::patch('/{nest}', [Application\Nests\NestController::class, 'update']); + + Route::delete('/{nest}', [Application\Nests\NestController::class, 'delete']); +}); + +/* +|-------------------------------------------------------------------------- +| Node Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/nodes +| +*/ +Route::group(['prefix' => '/nodes'], function () { + Route::get('/', [Application\Nodes\NodeController::class, 'index']); + Route::get('/deployable', [Application\Nodes\NodeDeploymentController::class, '__invoke']); + Route::get('/{node}', [Application\Nodes\NodeController::class, 'view']); + Route::get('/{node}/configuration', [Application\Nodes\NodeConfigurationController::class, '__invoke']); + Route::get('/{node}/information', [Application\Nodes\NodeInformationController::class, '__invoke']); + + Route::post('/', [Application\Nodes\NodeController::class, 'store']); + + Route::patch('/{node}', [Application\Nodes\NodeController::class, 'update']); + + Route::delete('/{node}', [Application\Nodes\NodeController::class, 'delete']); + + Route::group(['prefix' => '/{node}/allocations'], function () { + Route::get('/', [Application\Nodes\AllocationController::class, 'index']); + Route::post('/', [Application\Nodes\AllocationController::class, 'store']); + Route::delete('/{allocation}', [Application\Nodes\AllocationController::class, 'delete']); }); }); + +/* +|-------------------------------------------------------------------------- +| Role Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/roles +| +*/ +Route::group(['prefix' => '/roles'], function () { + Route::get('/', [Application\Roles\RoleController::class, 'index']); + Route::get('/{role}', [Application\Roles\RoleController::class, 'view']); + + Route::post('/', [Application\Roles\RoleController::class, 'store']); + + Route::patch('/{role}', [Application\Roles\RoleController::class, 'update']); + + Route::delete('/{role}', [Application\Roles\RoleController::class, 'delete']); +}); + +/* +|-------------------------------------------------------------------------- +| Server Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/servers +| +*/ +Route::group(['prefix' => '/servers'], function () { + Route::get('/', [Application\Servers\ServerController::class, 'index']); + Route::get('/{server}', [Application\Servers\ServerController::class, 'view']); + Route::get('/external/{external_id}', [Application\Servers\ExternalServerController::class, 'index']); + + Route::patch('/{server}', [Application\Servers\ServerController::class, 'update']); + Route::patch('/{server}/startup', [Application\Servers\StartupController::class, 'index']); + + Route::post('/', [Application\Servers\ServerController::class, 'store']); + Route::post('/{server}/suspend', [Application\Servers\ServerManagementController::class, 'suspend']); + Route::post('/{server}/unsuspend', [Application\Servers\ServerManagementController::class, 'unsuspend']); + Route::post('/{server}/reinstall', [Application\Servers\ServerManagementController::class, 'reinstall']); + + Route::delete('/{server}', [Application\Servers\ServerController::class, 'delete']); + Route::delete('/{server}/{force?}', [Application\Servers\ServerController::class, 'delete']); + + // Database Management Endpoint + Route::group(['prefix' => '/{server}/databases'], function () { + Route::get('/', [Application\Servers\DatabaseController::class, 'index']); + Route::get('/{database}', [Application\Servers\DatabaseController::class, 'view']); + + Route::post('/', [Application\Servers\DatabaseController::class, 'store']); + Route::post('/{database}/reset-password', [Application\Servers\DatabaseController::class, 'resetPassword']); + + Route::delete('/{database}', [Application\Servers\DatabaseController::class, 'delete']); + }); +}); + +/* +|-------------------------------------------------------------------------- +| User Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/users +| +*/ +Route::group(['prefix' => '/users'], function () { + Route::get('/', [Application\Users\UserController::class, 'index']); + Route::get('/{user}', [Application\Users\UserController::class, 'view']); + Route::get('/external/{external_id}', [Application\Users\ExternalUserController::class, 'index']); + + Route::post('/', [Application\Users\UserController::class, 'store']); + + Route::patch('/{user}', [Application\Users\UserController::class, 'update']); + + Route::delete('/{user}', [Application\Users\UserController::class, 'delete']); +}); diff --git a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php index 73a4f8f51..53d902e23 100644 --- a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php +++ b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php @@ -2,16 +2,13 @@ namespace Pterodactyl\Tests\Integration\Api\Application; -use Illuminate\Http\Request; use Pterodactyl\Models\User; -use PHPUnit\Framework\Assert; use Pterodactyl\Models\ApiKey; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Api\Transformer; use Pterodactyl\Tests\Integration\IntegrationTestCase; use Illuminate\Foundation\Testing\DatabaseTransactions; use Pterodactyl\Tests\Traits\Integration\CreatesTestModels; -use Pterodactyl\Transformers\Api\Application\BaseTransformer; -use Pterodactyl\Transformers\Api\Client\BaseClientTransformer; use Pterodactyl\Tests\Traits\Http\IntegrationJsonRequestAssertions; abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase @@ -95,18 +92,8 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase /** * Return a transformer that can be used for testing purposes. */ - protected function getTransformer(string $abstract): BaseTransformer + protected function getTransformer(string $abstract): Transformer { - $request = Request::createFromGlobals(); - $request->setUserResolver(function () { - return $this->getApiKey()->user; - }); - - $transformer = $abstract::fromRequest($request); - - Assert::assertInstanceOf(BaseTransformer::class, $transformer); - Assert::assertNotInstanceOf(BaseClientTransformer::class, $transformer); - - return $transformer; + return new $abstract(); } } diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index 6672960d3..428defd71 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -9,15 +9,12 @@ use Illuminate\Support\Facades\Event; use Pterodactyl\Events\ActivityLogged; use Pterodactyl\Tests\Assertions\AssertsActivityLogged; use Pterodactyl\Tests\Traits\Integration\CreatesTestModels; -use Pterodactyl\Transformers\Api\Application\BaseTransformer; abstract class IntegrationTestCase extends TestCase { use CreatesTestModels; use AssertsActivityLogged; -// protected array $connectionsToTransact = ['pgsql']; - protected $defaultHeaders = [ 'Accept' => 'application/json', ]; @@ -35,7 +32,7 @@ abstract class IntegrationTestCase extends TestCase protected function formatTimestamp(string $timestamp): string { return CarbonImmutable::createFromFormat(CarbonInterface::DEFAULT_TO_STRING_FORMAT, $timestamp) - ->setTimezone(BaseTransformer::RESPONSE_TIMEZONE) + ->setTimezone('UTC') ->toAtomString(); } } diff --git a/tests/Traits/Http/MocksMiddlewareClosure.php b/tests/Traits/Http/MocksMiddlewareClosure.php index 9cbe315bd..3519238bb 100644 --- a/tests/Traits/Http/MocksMiddlewareClosure.php +++ b/tests/Traits/Http/MocksMiddlewareClosure.php @@ -11,7 +11,7 @@ trait MocksMiddlewareClosure * Provide a closure to be used when validating that the response from the middleware * is the same request object we passed into it. */ - protected function getClosureAssertions(): \Closure + protected function getClosureAssertions(): Closure { if (is_null($this->request)) { throw new \BadFunctionCallException('Calling getClosureAssertions without defining a request object is not supported.');