diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php new file mode 100644 index 0000000..8dc2f56 --- /dev/null +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -0,0 +1,149 @@ +make(StartupCommandService::class); + + $user = $this->request->user(); + + return [ + 'server_owner' => $user->id === $server->owner_id, + 'identifier' => $server->uuidShort, + 'internal_id' => $server->id, + 'uuid' => $server->uuid, + 'name' => $server->name, + 'node' => $server->node->name, + 'is_node_under_maintenance' => $server->node->isUnderMaintenance(), + 'sftp_details' => [ + 'ip' => $server->node->fqdn, + 'port' => $server->node->daemonSFTP, + ], + 'description' => $server->description, + 'limits' => [ + 'memory' => $server->memory, + 'swap' => $server->swap, + 'disk' => $server->disk, + 'io' => $server->io, + 'cpu' => $server->cpu, + 'threads' => $server->threads, + 'oom_disabled' => $server->oom_disabled, + ], + 'invocation' => $service->handle($server, !$user->can(Permission::ACTION_STARTUP_READ, $server)), + 'docker_image' => $server->image, + 'egg_features' => $server->egg->inherit_features, + 'feature_limits' => [ + 'databases' => $server->database_limit, + 'allocations' => $server->allocation_limit, + '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), + + // Blueprint-related fields. + 'BlueprintFramework' => [ + 'egg_id' => $server->egg_id, + ], + ]; + } + + /** + * Returns the allocations associated with this server. + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeAllocations(Server $server): Collection + { + $transformer = $this->makeTransformer(AllocationTransformer::class); + + $user = $this->request->user(); + // While we include this permission, we do need to actually handle it slightly different here + // for the purpose of keeping things functionally working. If the user doesn't have read permissions + // for the allocations we'll only return the primary server allocation, and any notes associated + // with it will be hidden. + // + // This allows us to avoid too much permission regression, without also hiding information that + // is generally needed for the frontend to make sense when browsing or searching results. + if (!$user->can(Permission::ACTION_ALLOCATION_READ, $server)) { + $primary = clone $server->allocation; + $primary->notes = null; + + return $this->collection([$primary], $transformer, Allocation::RESOURCE_NAME); + } + + return $this->collection($server->allocations, $transformer, Allocation::RESOURCE_NAME); + } + + /** + * @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 + ); + } + + /** + * 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); + } + + /** + * Returns the subusers associated with this server. + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeSubusers(Server $server): Collection|NullResource + { + if (!$this->request->user()->can(Permission::ACTION_USER_READ, $server)) { + return $this->null(); + } + + return $this->collection($server->subusers, $this->makeTransformer(SubuserTransformer::class), Subuser::RESOURCE_NAME); + } +} diff --git a/resources/scripts/api/server/getServer.ts b/resources/scripts/api/server/getServer.ts new file mode 100644 index 0000000..860c423 --- /dev/null +++ b/resources/scripts/api/server/getServer.ts @@ -0,0 +1,93 @@ +import http, { FractalResponseData, FractalResponseList } from '@/api/http'; +import { rawDataToServerAllocation, rawDataToServerEggVariable } from '@/api/transformers'; +import { ServerEggVariable, ServerStatus } from '@/api/server/types'; + +export interface Allocation { + id: number; + ip: string; + alias: string | null; + port: number; + notes: string | null; + isDefault: boolean; +} + +export interface Server { + id: string; + internalId: number | string; + uuid: string; + name: string; + node: string; + isNodeUnderMaintenance: boolean; + status: ServerStatus; + sftpDetails: { + ip: string; + port: number; + }; + invocation: string; + dockerImage: string; + description: string; + limits: { + memory: number; + swap: number; + disk: number; + io: number; + cpu: number; + threads: string; + }; + eggFeatures: string[]; + featureLimits: { + databases: number; + allocations: number; + backups: number; + }; + isTransferring: boolean; + variables: ServerEggVariable[]; + allocations: Allocation[]; + + BlueprintFramework: { + eggId: number; + }; +} + +export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({ + id: data.identifier, + internalId: data.internal_id, + uuid: data.uuid, + name: data.name, + node: data.node, + isNodeUnderMaintenance: data.is_node_under_maintenance, + status: data.status, + invocation: data.invocation, + dockerImage: data.docker_image, + sftpDetails: { + ip: data.sftp_details.ip, + port: data.sftp_details.port, + }, + description: data.description ? (data.description.length > 0 ? data.description : null) : null, + limits: { ...data.limits }, + eggFeatures: data.egg_features || [], + featureLimits: { ...data.feature_limits }, + isTransferring: data.is_transferring, + variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map( + rawDataToServerEggVariable + ), + allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map( + rawDataToServerAllocation + ), + + BlueprintFramework: { ...data.BlueprintFramework } +}); + +export default (uuid: string): Promise<[Server, string[]]> => { + return new Promise((resolve, reject) => { + http.get(`/api/client/servers/${uuid}`) + .then(({ data }) => + resolve([ + rawDataToServerObject(data), + // eslint-disable-next-line camelcase + data.meta?.is_server_owner ? ['*'] : data.meta?.user_permissions || [], + ]) + ) + .catch(reject); + }); +}; diff --git a/resources/scripts/blueprint/extends/routers/ServerRouter.tsx b/resources/scripts/blueprint/extends/routers/ServerRouter.tsx index 79972f5..4114d16 100644 --- a/resources/scripts/blueprint/extends/routers/ServerRouter.tsx +++ b/resources/scripts/blueprint/extends/routers/ServerRouter.tsx @@ -14,7 +14,7 @@ import blueprintRoutes from './routes'; export const NavigationLinks = () => { const rootAdmin = useStoreState((state) => state.user.data!.rootAdmin); - const serverEgg = ServerContext.useStoreState((state) => state.server.data?.eggId); + const serverEgg = ServerContext.useStoreState((state) => state.server.data?.BlueprintFramework.eggId); const match = useRouteMatch<{ id: string }>(); const to = (value: string, url = false) => { if (value === '/') { @@ -70,7 +70,7 @@ export const NavigationLinks = () => { export const NavigationRouter = () => { const rootAdmin = useStoreState((state) => state.user.data!.rootAdmin); - const serverEgg = ServerContext.useStoreState((state) => state.server.data?.eggId); + const serverEgg = ServerContext.useStoreState((state) => state.server.data?.BlueprintFramework.eggId); const match = useRouteMatch<{ id: string }>(); const to = (value: string, url = false) => { if (value === '/') {