mirror of https://github.com/Duxez/PteroPack.git
commit
d0f914afc3
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\CurseForge;
|
||||
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Http\Request;
|
||||
use Nette\NotImplementedException;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Jobs\ModInstall;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
|
||||
|
||||
class ModpackController extends ClientApiController {
|
||||
private $http_client;
|
||||
private $minecraft_game_id;
|
||||
private $modpack_class_id;
|
||||
private $api_key;
|
||||
private DaemonFileRepository $fileRepository;
|
||||
|
||||
|
||||
public function __construct(DaemonFileRepository $fileRepository) {
|
||||
parent::__construct();
|
||||
|
||||
if(!config('curseforge.api_key')) {
|
||||
throw new DisplayException('CurseForge API key is not set.');
|
||||
}
|
||||
|
||||
$this->http_client = new Client([
|
||||
'base_uri' => 'https://api.curseforge.com/v1/',
|
||||
'headers' => [
|
||||
'x-api-key' => config('curseforge.api_key'),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->api_key = config('curseforge.api_key');
|
||||
$this->minecraft_game_id = config('curseforge.minecraft_game_id');
|
||||
$this->modpack_class_id = config('curseforge.minecraft_modpack_class_id');
|
||||
|
||||
$this->fileRepository = $fileRepository;
|
||||
}
|
||||
|
||||
public function index(Request $request) {
|
||||
$index = 0;
|
||||
if($request->has('pageindex')) {
|
||||
$index = $request->get('pageindex');
|
||||
}
|
||||
|
||||
$queryString = '';
|
||||
if($request->has('modloader') && $request->get('modloader') !== '0') {
|
||||
$queryString .= '&modloadertype=' . $request->get('modloader');
|
||||
}
|
||||
|
||||
$result = $this->http_client->get("mods/search?gameid=$this->minecraft_game_id&classid=$this->modpack_class_id&sortField=2&sortorder=desc&index=$index$queryString");
|
||||
|
||||
if($result->getStatusCode() !== 200) {
|
||||
throw new DisplayException('Failed to fetch modpacks from CurseForge.');
|
||||
}
|
||||
|
||||
return $result->getBody()->getContents();
|
||||
}
|
||||
|
||||
public function description(Request $request, $server, $modpack) {
|
||||
|
||||
$result = $this->http_client->get("mods/$modpack/description");
|
||||
|
||||
if($result->getStatusCode() !== 200) {
|
||||
throw new DisplayException('Failed to fetch modpack description from CurseForge.');
|
||||
}
|
||||
|
||||
return $result->getBody()->getContents();
|
||||
}
|
||||
|
||||
public function show() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public function install($server, $modId, $fileId) {
|
||||
$this->fileRepository->setServer($server)->deleteFiles('/', ['mods']);
|
||||
$this->fileRepository->setServer($server)->deleteFiles('/', ['uninstallable.txt']);
|
||||
|
||||
$modpackFile = json_decode($this->getFileById($modId, $fileId));
|
||||
|
||||
$data = $modpackFile->data;
|
||||
$modpackFileUrl = $this->traceUrl($data->downloadUrl);
|
||||
|
||||
$this->fileRepository->setServer($server)->pull(
|
||||
$modpackFileUrl,
|
||||
'/',
|
||||
['filename' => 'modpack.zip', 'foreground' => true]
|
||||
);
|
||||
|
||||
$this->fileRepository->setServer($server)->decompressFile('', 'modpack.zip');
|
||||
$modpackManifest = json_decode($this->fileRepository->setServer($server)->getContent('manifest.json'));
|
||||
|
||||
$job = ModInstall::dispatch($modpackManifest, $server, $this->api_key);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function traceUrl(string $url): string {
|
||||
$httpCode = '';
|
||||
while (!str_contains($httpCode, '200 OK')) {
|
||||
$headers = get_headers($url, 5);
|
||||
$httpCode = $headers[0];
|
||||
if (array_key_exists('Location', $headers) && !empty($headers['Location'])) {
|
||||
$url = $headers['Location'];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
private function getModById(int $modId): string {
|
||||
$result = $this->http_client->get("mods/$modId");
|
||||
|
||||
return $result->getBody()->getContents();
|
||||
}
|
||||
|
||||
private function getFileById(int $modId, int $fileId): string {
|
||||
$result = $this->http_client->get("mods/$modId/files/$fileId");
|
||||
|
||||
return $result->getBody()->getContents();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Services\Nodes\NodeJWTService;
|
||||
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\FileObjectTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\PullFileRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ChmodFilesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CompressFilesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DecompressFilesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest;
|
||||
|
||||
class FileController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* FileController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private NodeJWTService $jwtService,
|
||||
private DaemonFileRepository $fileRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a listing of files in a given directory.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function directory(ListFilesRequest $request, Server $server): array
|
||||
{
|
||||
$contents = $this->fileRepository
|
||||
->setServer($server)
|
||||
->getDirectory($request->get('directory') ?? '/');
|
||||
|
||||
return $this->fractal->collection($contents)
|
||||
->transformWith($this->getTransformer(FileObjectTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of a specified file for the user.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function contents(GetFileContentsRequest $request, Server $server): Response
|
||||
{
|
||||
$response = $this->fileRepository->setServer($server)->getContent(
|
||||
$request->get('file'),
|
||||
config('pterodactyl.files.max_edit_size')
|
||||
);
|
||||
|
||||
Activity::event('server:file.read')->property('file', $request->get('file'))->log();
|
||||
|
||||
return new Response($response, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if file exists, false if it does not.
|
||||
*/
|
||||
public function exists(GetFileContentsRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
try {
|
||||
$response = $this->fileRepository->setServer($server)->getContent(
|
||||
$request->get('file'),
|
||||
config('pterodactyl.files.max_edit_size')
|
||||
);
|
||||
|
||||
Activity::event('server:file.exists')->property('file', $request->get('file'))->log();
|
||||
|
||||
return new JsonResponse($response !== '');
|
||||
} catch (\Exception $exception) {
|
||||
return new JsonResponse(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a one-time token with a link that the user can use to
|
||||
* download a given file.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function download(GetFileContentsRequest $request, Server $server): array
|
||||
{
|
||||
$token = $this->jwtService
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setUser($request->user())
|
||||
->setClaims([
|
||||
'file_path' => rawurldecode($request->get('file')),
|
||||
'server_uuid' => $server->uuid,
|
||||
])
|
||||
->handle($server->node, $request->user()->id . $server->uuid);
|
||||
|
||||
Activity::event('server:file.download')->property('file', $request->get('file'))->log();
|
||||
|
||||
return [
|
||||
'object' => 'signed_url',
|
||||
'attributes' => [
|
||||
'url' => sprintf(
|
||||
'%s/download/file?token=%s',
|
||||
$server->node->getConnectionAddress(),
|
||||
$token->toString()
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the contents of the specified file to the server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function write(WriteFileContentRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository->setServer($server)->putContent($request->get('file'), $request->getContent());
|
||||
|
||||
Activity::event('server:file.write')->property('file', $request->get('file'))->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new folder on the server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function create(CreateFolderRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository
|
||||
->setServer($server)
|
||||
->createDirectory($request->input('name'), $request->input('root', '/'));
|
||||
|
||||
Activity::event('server:file.create-directory')
|
||||
->property('name', $request->input('name'))
|
||||
->property('directory', $request->input('root'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a file on the remote machine.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function rename(RenameFileRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository
|
||||
->setServer($server)
|
||||
->renameFiles($request->input('root'), $request->input('files'));
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $request->input('root'))
|
||||
->property('files', $request->input('files'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a file on the server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function copy(CopyFileRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository
|
||||
->setServer($server)
|
||||
->copyFile($request->input('location'));
|
||||
|
||||
Activity::event('server:file.copy')->property('file', $request->input('location'))->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function compress(CompressFilesRequest $request, Server $server): array
|
||||
{
|
||||
$file = $this->fileRepository->setServer($server)->compressFiles(
|
||||
$request->input('root'),
|
||||
$request->input('files')
|
||||
);
|
||||
|
||||
Activity::event('server:file.compress')
|
||||
->property('directory', $request->input('root'))
|
||||
->property('files', $request->input('files'))
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($file)
|
||||
->transformWith($this->getTransformer(FileObjectTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function decompress(DecompressFilesRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
set_time_limit(300);
|
||||
|
||||
$this->fileRepository->setServer($server)->decompressFile(
|
||||
$request->input('root'),
|
||||
$request->input('file')
|
||||
);
|
||||
|
||||
Activity::event('server:file.decompress')
|
||||
->property('directory', $request->input('root'))
|
||||
->property('files', $request->input('file'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes files or folders for the server in the given root directory.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function delete(DeleteFileRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository->setServer($server)->deleteFiles(
|
||||
$request->input('root'),
|
||||
$request->input('files')
|
||||
);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $request->input('root'))
|
||||
->property('files', $request->input('files'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates file permissions for file(s) in the given root directory.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function chmod(ChmodFilesRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository->setServer($server)->chmodFiles(
|
||||
$request->input('root'),
|
||||
$request->input('files')
|
||||
);
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests that a file be downloaded from a remote location by Wings.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function pull(PullFileRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->fileRepository->setServer($server)->pull(
|
||||
$request->input('url'),
|
||||
$request->input('directory'),
|
||||
$request->safe(['filename', 'use_header', 'foreground'])
|
||||
);
|
||||
|
||||
Activity::event('server:file.pull')
|
||||
->property('directory', $request->input('directory'))
|
||||
->property('url', $request->input('url'))
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Jobs;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
|
||||
|
||||
class ModInstall implements ShouldQueue {
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private $http_client;
|
||||
private $api_key;
|
||||
private $modpackManifest;
|
||||
private $server;
|
||||
|
||||
public function __construct(object $modpackManifest, $server, $api_key){
|
||||
$this->api_key = $api_key;
|
||||
$this->modpackManifest = $modpackManifest;
|
||||
$this->server = $server;
|
||||
}
|
||||
|
||||
public function handle(DaemonFileRepository $fileRepository) {
|
||||
$this->http_client = new Client([
|
||||
'base_uri' => 'https://api.curseforge.com/v1/',
|
||||
'headers' => [
|
||||
'x-api-key' => $this->api_key,
|
||||
]
|
||||
]);
|
||||
|
||||
$uninstallable = [];
|
||||
|
||||
foreach($this->modpackManifest->files as $file) {
|
||||
$modFile = json_decode($this->getFileById($file->projectID, $file->fileID));
|
||||
$modFileUrl = $this->traceUrl($modFile->data->downloadUrl);
|
||||
|
||||
if($modFileUrl == null) {
|
||||
$uninstallable[] = $modFile->data->fileName;
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileRepository->setServer($this->server)->pull(
|
||||
$modFileUrl,
|
||||
'mods',
|
||||
['filename' => $modFile->data->fileName, 'foreground' => true]
|
||||
);
|
||||
}
|
||||
|
||||
$fileRepository->putContent('uninstallable.txt', print_r($uninstallable));
|
||||
$fileRepository->deleteFiles('/', ['modpack.zip', 'manifest.json', $this->modpackManifest->overrides]);
|
||||
}
|
||||
|
||||
private function getFileById(int $modId, int $fileId): string {
|
||||
$result = $this->http_client->get("mods/$modId/files/$fileId");
|
||||
|
||||
return $result->getBody()->getContents();
|
||||
}
|
||||
|
||||
private function traceUrl(string $url): string {
|
||||
$httpCode = '';
|
||||
while (!str_contains($httpCode, '200 OK')) {
|
||||
$headers = get_headers($url, 5);
|
||||
$httpCode = $headers[0];
|
||||
if (array_key_exists('Location', $headers) && !empty($headers['Location'])) {
|
||||
$url = $headers['Location'];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'api_key' => env('CURSEFORGE_API_KEY'),
|
||||
'minecraft_game_id' => '432',
|
||||
'minecraft_modpack_class_id' => '4471',
|
||||
];
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export default (server: string, file: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${server}/files/exists`, {
|
||||
params: { file },
|
||||
})
|
||||
.then(({ data }) => resolve(data))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export default (server: string, pullFileRequest: PullFileRequest): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(`/api/client/servers/${server}/files/pull`, {
|
||||
url: encodeURI(pullFileRequest.url),
|
||||
directory: pullFileRequest.directory,
|
||||
filename: pullFileRequest.filename,
|
||||
use_header: pullFileRequest.use_header,
|
||||
foreground: pullFileRequest.foreground,
|
||||
})
|
||||
.then(({ data }) => resolve(data))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export interface PullFileRequest {
|
||||
url: string;
|
||||
directory: string;
|
||||
filename: string;
|
||||
use_header: boolean;
|
||||
foreground: boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
|
||||
export interface Modpack {
|
||||
id: number;
|
||||
gameId: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
links: Link;
|
||||
summary: string;
|
||||
status: number;
|
||||
downloadCount: number;
|
||||
isFeatured: boolean;
|
||||
primaryCategoryId: number;
|
||||
categories: Category[];
|
||||
classId: number;
|
||||
authors: Author[];
|
||||
logo: Image;
|
||||
screenshots: Image[];
|
||||
mainFileId: number;
|
||||
latestFiles: File[];
|
||||
latestFileIndexes: FileIndex[];
|
||||
latestEarlyAccessFileIndexes: FileIndex[];
|
||||
dateCreated: string;
|
||||
dateModified: string;
|
||||
dateReleased: string;
|
||||
allowModDistribution: boolean;
|
||||
gamePopularityRank: number;
|
||||
isAvailable: boolean;
|
||||
thumbsUpCount: number;
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
websiteUrl: string;
|
||||
wikiUrl: string|null;
|
||||
issuesUrl: string|null;
|
||||
sourceUrl: string|null;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
gameId: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
iconUrl: string;
|
||||
dateModified: string;
|
||||
isClass: boolean;
|
||||
classId: number;
|
||||
parentCategoryId: number;
|
||||
}
|
||||
|
||||
export interface Author {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
id: number;
|
||||
modId: number;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnailUrl: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface File {
|
||||
id: number;
|
||||
gameId: number;
|
||||
modId: number;
|
||||
isAvailable: boolean;
|
||||
displayName: string;
|
||||
fileName: string;
|
||||
releaseType: number;
|
||||
fileStatus: number;
|
||||
hashes: Hash[];
|
||||
fileDate: string;
|
||||
fileLength: number;
|
||||
downloadCount: number;
|
||||
downloadUrl: string;
|
||||
gameVersions: string[];
|
||||
sortableGameVersion: GameVersion[];
|
||||
dependencies: any[];
|
||||
alternateFileId: number;
|
||||
isServerPack: boolean;
|
||||
fileFingerprint: number;
|
||||
modules: any[];
|
||||
}
|
||||
|
||||
export interface Hash {
|
||||
value: string;
|
||||
algorithm: number;
|
||||
}
|
||||
|
||||
export interface GameVersion {
|
||||
gameVersionName: string;
|
||||
gameVersionPadded: string;
|
||||
gameVersion: string;
|
||||
gameVersionReleaseDate: string;
|
||||
gameVersionTypeID: number;
|
||||
}
|
||||
|
||||
export interface FileIndex {
|
||||
gameVersion: string;
|
||||
fileId: number;
|
||||
filename: string;
|
||||
releaseType: number;
|
||||
gameVersionTypeId: number;
|
||||
modLoader: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import http from "@/api/http";
|
||||
|
||||
export default (uuid: string, id: number): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
console.log(uuid, id);
|
||||
|
||||
http.get(`/api/client/servers/${uuid}/modpacks/${id}/description`)
|
||||
.then((response) => {
|
||||
resolve(response.data.data)
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import http, { PaginationDataSet } from '@/api/http'
|
||||
import { Modpack } from './Modpack';
|
||||
import { ModpackSearchFilter } from '@/components/server/modpacks/ModpackContainer';
|
||||
|
||||
export default (uuid: string, pageIndex: number = 0, filters: ModpackSearchFilter): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let filterQuery = '&';
|
||||
if(filters.modloaderType) {
|
||||
filterQuery += `modloader=${filters.modloaderType}`;
|
||||
} else {
|
||||
filterQuery += `modloader=0`;
|
||||
}
|
||||
|
||||
http.get(`/api/client/servers/${uuid}/modpacks?pageindex=${pageIndex}${filterQuery}`)
|
||||
.then((response) => {
|
||||
resolve([(response.data.data || []).map((item: any) => rawDataToModpackData(item)), rawDataToModpackPaginationData(response.data.pagination), {modloaderType: filters.modloaderType}])
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
export const rawDataToModpackPaginationData = (data: any): PaginationDataSet => ({
|
||||
total: data.totalCount,
|
||||
count: data.resultCount,
|
||||
perPage: data.pageSize,
|
||||
currentPage: Math.ceil((data.index + data.pageSize) / data.pageSize) == 0 ? 1 : Math.ceil((data.index + data.pageSize) / data.pageSize),
|
||||
totalPages: Math.ceil(data.totalCount / data.pageSize)
|
||||
});
|
||||
|
||||
export const rawDataToModpackData = (data: any): Modpack => ({
|
||||
id: data.id,
|
||||
gameId: data.gameId,
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
links: data.links,
|
||||
summary: data.summary,
|
||||
status: data.status,
|
||||
downloadCount: data.downloadCount,
|
||||
isFeatured: data.isFeatured,
|
||||
primaryCategoryId: data.primaryCategoryId,
|
||||
categories: data.categories,
|
||||
classId: data.classId,
|
||||
authors: data.authors,
|
||||
logo: data.logo,
|
||||
screenshots: data.screenshots,
|
||||
mainFileId: data.mainFileId,
|
||||
latestFiles: data.latestFiles,
|
||||
latestFileIndexes: data.latestFileIndexes,
|
||||
latestEarlyAccessFileIndexes: data.latestEarlyAccessFileIndexes,
|
||||
dateCreated: data.dateCreated,
|
||||
dateModified: data.dateModified,
|
||||
dateReleased: data.dateReleased,
|
||||
allowModDistribution: data.allowModDistribution,
|
||||
gamePopularityRank: data.gamePopularityRank,
|
||||
isAvailable: data.isAvailable,
|
||||
thumbsUpCount: data.thumbsUpCount,
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import http from "@/api/http";
|
||||
|
||||
export default (server: string, modpackId: number, fileId: number): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${server}/modpacks/${modpackId}/install/${fileId}`)
|
||||
.then(({ data }) => {
|
||||
console.log(data);
|
||||
resolve(data)
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
import { PaginatedResult } from '@/api/http';
|
||||
import tw from 'twin.macro';
|
||||
import styled from 'styled-components/macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAngleDoubleLeft, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface RenderFuncProps<T> {
|
||||
items: T[];
|
||||
isLastPage: boolean;
|
||||
isFirstPage: boolean;
|
||||
}
|
||||
|
||||
interface Props<T> {
|
||||
data: PaginatedResult<T>;
|
||||
showGoToLast?: boolean;
|
||||
showGoToFirst?: boolean;
|
||||
onPageSelect: (page: number) => void;
|
||||
children: (props: RenderFuncProps<T>) => React.ReactNode;
|
||||
paginationButtonsClassNames?: string;
|
||||
}
|
||||
|
||||
const Block = styled(Button)`
|
||||
${tw`p-0 w-10 h-10`}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
${tw`mr-2`};
|
||||
}
|
||||
`;
|
||||
|
||||
function Pagination<T>({ data: { items, pagination }, onPageSelect, children, paginationButtonsClassNames}: Props<T>) {
|
||||
const isFirstPage = pagination.currentPage === 1;
|
||||
const isLastPage = pagination.currentPage >= pagination.totalPages;
|
||||
|
||||
const pages = [];
|
||||
|
||||
// Start two spaces before the current page. If that puts us before the starting page default
|
||||
// to the first page as the starting point.
|
||||
const start = Math.max(pagination.currentPage - 2, 1);
|
||||
const end = Math.min(pagination.totalPages, pagination.currentPage + 5);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children({ items, isFirstPage, isLastPage })}
|
||||
<div className={paginationButtonsClassNames}>
|
||||
{pages.length > 1 && (
|
||||
<div css={tw`mt-4 flex justify-center`}>
|
||||
{pages[0] > 1 && !isFirstPage && (
|
||||
<Block isSecondary color={'primary'} onClick={() => onPageSelect(1)}>
|
||||
<FontAwesomeIcon icon={faAngleDoubleLeft} />
|
||||
</Block>
|
||||
)}
|
||||
{pages.map((i) => (
|
||||
<Block
|
||||
isSecondary={pagination.currentPage !== i}
|
||||
color={'primary'}
|
||||
key={`block_page_${i}`}
|
||||
onClick={() => onPageSelect(i)}
|
||||
>
|
||||
{i}
|
||||
</Block>
|
||||
))}
|
||||
{pages[4] < pagination.totalPages && !isLastPage && (
|
||||
<Block isSecondary color={'primary'} onClick={() => onPageSelect(pagination.totalPages)}>
|
||||
<FontAwesomeIcon icon={faAngleDoubleRight} />
|
||||
</Block>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Pagination;
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import ServerContentBlock from "@/components/elements/ServerContentBlock";
|
||||
import ModpackItem from './ModpackItem';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import getModpacks from '@/api/server/modpacks/getModpacks';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import tw from 'twin.macro';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
import { Modpack } from '@/api/server/modpacks/Modpack';
|
||||
import Pagination from '@/components/elements/Pagination';
|
||||
import { PaginatedResult } from '@/api/http';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import { Dropdown } from '@/components/elements/dropdown';
|
||||
import Select from '@/components/elements/Select';
|
||||
import { set } from 'date-fns';
|
||||
|
||||
export enum ModloaderType {
|
||||
Any,
|
||||
Forge,
|
||||
Cauldron,
|
||||
LiteLoader,
|
||||
Fabric,
|
||||
Quilt
|
||||
}
|
||||
|
||||
export interface ModpackSearchFilter {
|
||||
modloaderType: ModloaderType;
|
||||
}
|
||||
|
||||
export default() => {
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const serverData = ServerContext.useStoreState((state) => state.server.data);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [modloaderType, setModloaderType] = useState<ModloaderType>(ModloaderType.Any);
|
||||
const [filters, setFilters] = useState<ModpackSearchFilter>({modloaderType: ModloaderType.Any});
|
||||
|
||||
const [modpacks, setModpacks] = useState<PaginatedResult<Modpack>>();
|
||||
|
||||
const changePage = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
setLoading(true);
|
||||
|
||||
let pageIndex = 0;
|
||||
if(modpacks?.pagination.perPage != undefined) {
|
||||
pageIndex = (modpacks.pagination.perPage * newPage) - modpacks.pagination.perPage;
|
||||
console.log(pageIndex, modpacks.pagination.perPage, newPage);
|
||||
}
|
||||
|
||||
getModpacks(uuid, pageIndex, filters)
|
||||
.then((modpacksResult) => {
|
||||
setModpacks({items: modpacksResult[0], pagination: modpacksResult[1]});
|
||||
setModloaderType(modpacksResult[2].modloaderType);
|
||||
setLoading(false);
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!modpacks?.items.length);
|
||||
|
||||
|
||||
changePage(1);
|
||||
}, [modloaderType]);
|
||||
|
||||
const updateFilterModloaderType = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setFilters({modloaderType: parseInt(e.target.value)});
|
||||
setModloaderType(parseInt(e.target.value));
|
||||
},
|
||||
[modloaderType],
|
||||
);
|
||||
|
||||
return (
|
||||
<ServerContentBlock title="Modpacks">
|
||||
<FlashMessageRender byKey={'modpacks'} css={tw`mb-4`} />
|
||||
{(modpacks == undefined || !modpacks?.items.length) && loading ? (
|
||||
<Spinner size={'large'} centered />
|
||||
) : (
|
||||
<Fade timeout={150}>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<TitledGreyBox title='Filters' className='col-span-3'>
|
||||
<GreyRowBox>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<Select defaultValue={modloaderType} onChange={updateFilterModloaderType}>
|
||||
<option value={0}>
|
||||
Any
|
||||
</option>
|
||||
<option value={1}>
|
||||
Forge
|
||||
</option>
|
||||
<option value={2}>
|
||||
Cauldron
|
||||
</option>
|
||||
<option value={3}>
|
||||
LiteLoader
|
||||
</option>
|
||||
<option value={4}>
|
||||
Fabric
|
||||
</option>
|
||||
<option value={5}>
|
||||
Quilt
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
</GreyRowBox>
|
||||
</TitledGreyBox>
|
||||
<Pagination data={modpacks as PaginatedResult<Modpack>} onPageSelect={changePage} paginationButtonsClassNames='col-span-3'>
|
||||
{({ items }) => (
|
||||
items.length > 0 ? (
|
||||
modpacks?.items.map((modpack, index) => (
|
||||
<ModpackItem
|
||||
key={modpack.id}
|
||||
modpack={modpack}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p css={tw`text-center text-sm text-neutral-300`}>
|
||||
Couldn't fetch modpacks.
|
||||
</p>
|
||||
))}
|
||||
</Pagination>
|
||||
</div>
|
||||
</Fade>
|
||||
)}
|
||||
</ServerContentBlock>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
import { File, Modpack } from '@/api/server/modpacks/Modpack';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import React from 'react';
|
||||
import ModpackModal from './ModpackModal';
|
||||
import Button from '@/components/elements/Button';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faDownload, faGamepad } from '@fortawesome/free-solid-svg-icons';
|
||||
import getModpackDescription from '@/api/server/modpacks/getModpackDescription';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import installModpack from '@/api/server/modpacks/installModpack';
|
||||
import getFileExists from '@/api/server/files/getFileExists';
|
||||
|
||||
interface Props {
|
||||
modpack: Modpack;
|
||||
}
|
||||
|
||||
interface ModpackManifest {
|
||||
author: string;
|
||||
files: ModpackFile[]
|
||||
manifestType: string;
|
||||
manifestVersion: number;
|
||||
minecraft: {};
|
||||
name: string;
|
||||
overrides: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface ModpackFile {
|
||||
projectID: number;
|
||||
fileID: number;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
const timer = (ms: number) => new Promise(res => setTimeout(res, ms))
|
||||
|
||||
export default ({ modpack }: Props) => {
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [installVisible, setInstallVisible] = React.useState(false);
|
||||
const [installed, setInstalled] = React.useState(false);
|
||||
const [modpackDesc, setModpackDesc] = React.useState<string>('');
|
||||
const [installing, setInstalling] = React.useState(false);
|
||||
|
||||
const openModal = (): void => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const openInstallModal = (): void => {
|
||||
setInstallVisible(true);
|
||||
};
|
||||
|
||||
const install = async (latestFile: File): Promise<void> => {
|
||||
setInstalling(true);
|
||||
|
||||
await installModpack(uuid, modpack.id, latestFile.id);
|
||||
|
||||
let uninstallableExists = false;
|
||||
while (!uninstallableExists) {
|
||||
uninstallableExists = await getFileExists(uuid, 'uninstallable.txt') as unknown as boolean;
|
||||
await timer(6500);
|
||||
}
|
||||
|
||||
setInstalling(false);
|
||||
setInstalled(true);
|
||||
};
|
||||
|
||||
const getDescription = (): void => {
|
||||
getModpackDescription(uuid, modpack.id).then((description) => setModpackDesc(description));
|
||||
};
|
||||
|
||||
const openSite = (): void => {
|
||||
throw new Error('Function not implemented.');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TitledGreyBox title={modpack.name}>
|
||||
<GreyRowBox onClick={openModal}>
|
||||
<img style={{ width: '50px', marginRight: '10px' }} src={modpack.logo.url} />
|
||||
<p>{modpack.summary}</p>
|
||||
</GreyRowBox>
|
||||
</TitledGreyBox>
|
||||
|
||||
<ModpackModal visible={visible} onDismissed={() => setVisible(false)} modpack={modpack}>
|
||||
<div className='grid grid-cols-3 gap-2' style={{ maxHeight: '70vh' }}>
|
||||
<img src={modpack.logo.url} style={{ width: '150px' }} />
|
||||
<h1 style={{ fontSize: '1.5em' }}>{modpack.name}</h1>
|
||||
<Button style={{ height: '50px' }} onClick={openInstallModal}>
|
||||
Install
|
||||
</Button>
|
||||
<p className='col-span-3'>{modpack.summary}</p>
|
||||
{modpack.screenshots.map((screenshot) => (
|
||||
<img src={screenshot.url} />
|
||||
))}
|
||||
<div className='flex items-center'>
|
||||
<FontAwesomeIcon icon={faDownload} style={{ marginRight: '10px' }} />
|
||||
{modpack.downloadCount}
|
||||
</div>
|
||||
<div className='flex items-center'></div>
|
||||
<Button onClick={openSite}>Website</Button>
|
||||
</div>
|
||||
</ModpackModal>
|
||||
|
||||
<ModpackModal visible={installVisible} onDismissed={() => setInstallVisible(false)} modpack={modpack}>
|
||||
<div style={{ maxHeight: '50vh' }}>
|
||||
{installing ? (
|
||||
<div>
|
||||
<p>Installing... This can take a while, sit back, grab a drink and relax</p>
|
||||
<Spinner centered size='base' />
|
||||
</div>
|
||||
) : (<>{installed ? (
|
||||
<p>Modpack insatlled, if any mods couldn't be manually installed you can find them in the file 'uninstallable.txt', install them manually before playing.</p>
|
||||
) : (
|
||||
modpack.latestFiles.map((file) => (
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<p>{file.displayName}</p>
|
||||
<div className='flex items-center'>
|
||||
<FontAwesomeIcon icon={faGamepad} style={{ marginRight: '10px' }} />
|
||||
{file.gameVersions[0]}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
install(file);
|
||||
}}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModpackModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Pterodactyl\Http\Controllers\Api\Client;
|
||||
use Pterodactyl\Http\Middleware\Activity\ServerSubject;
|
||||
use Pterodactyl\Http\Middleware\Activity\AccountSubject;
|
||||
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
use Pterodactyl\Http\Middleware\Api\Client\Server\ResourceBelongsToServer;
|
||||
use Pterodactyl\Http\Middleware\Api\Client\Server\AuthenticateServerAccess;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Client Control API
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoint: /api/client
|
||||
|
|
||||
*/
|
||||
Route::get('/', [Client\ClientController::class, 'index'])->name('api:client.index');
|
||||
Route::get('/permissions', [Client\ClientController::class, 'permissions']);
|
||||
|
||||
Route::prefix('/account')->middleware(AccountSubject::class)->group(function () {
|
||||
Route::prefix('/')->withoutMiddleware(RequireTwoFactorAuthentication::class)->group(function () {
|
||||
Route::get('/', [Client\AccountController::class, 'index'])->name('api:client.account');
|
||||
Route::get('/two-factor', [Client\TwoFactorController::class, 'index']);
|
||||
Route::post('/two-factor', [Client\TwoFactorController::class, 'store']);
|
||||
Route::delete('/two-factor', [Client\TwoFactorController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::put('/email', [Client\AccountController::class, 'updateEmail'])->name('api:client.account.update-email');
|
||||
Route::put('/password', [Client\AccountController::class, 'updatePassword'])->name('api:client.account.update-password');
|
||||
|
||||
Route::get('/activity', Client\ActivityLogController::class)->name('api:client.account.activity');
|
||||
|
||||
Route::get('/api-keys', [Client\ApiKeyController::class, 'index']);
|
||||
Route::post('/api-keys', [Client\ApiKeyController::class, 'store']);
|
||||
Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']);
|
||||
|
||||
Route::prefix('/ssh-keys')->group(function () {
|
||||
Route::get('/', [Client\SSHKeyController::class, 'index']);
|
||||
Route::post('/', [Client\SSHKeyController::class, 'store']);
|
||||
Route::post('/remove', [Client\SSHKeyController::class, 'delete']);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Client Control API
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoint: /api/client/servers/{server}
|
||||
|
|
||||
*/
|
||||
Route::group([
|
||||
'prefix' => '/servers/{server}',
|
||||
'middleware' => [
|
||||
ServerSubject::class,
|
||||
AuthenticateServerAccess::class,
|
||||
ResourceBelongsToServer::class,
|
||||
],
|
||||
], function () {
|
||||
Route::get('/', [Client\Servers\ServerController::class, 'index'])->name('api:client:server.view');
|
||||
Route::get('/websocket', Client\Servers\WebsocketController::class)->name('api:client:server.ws');
|
||||
Route::get('/resources', Client\Servers\ResourceUtilizationController::class)->name('api:client:server.resources');
|
||||
Route::get('/activity', Client\Servers\ActivityLogController::class)->name('api:client:server.activity');
|
||||
|
||||
Route::post('/command', [Client\Servers\CommandController::class, 'index']);
|
||||
Route::post('/power', [Client\Servers\PowerController::class, 'index']);
|
||||
|
||||
Route::group(['prefix' => '/modpacks'], function () {
|
||||
Route::get('/', [Client\Servers\CurseForge\ModpackController::class, 'index']);
|
||||
Route::get('/{modpack}', [Client\Servers\CurseForge\ModpackController::class, 'show']);
|
||||
Route::get('/{modpack}/install/{file}', [Client\Servers\CurseForge\ModpackController::class, 'install']);
|
||||
Route::get('/{modpack}/description', [Client\Servers\CurseForge\ModpackController::class, 'description']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/databases'], function () {
|
||||
Route::get('/', [Client\Servers\DatabaseController::class, 'index']);
|
||||
Route::post('/', [Client\Servers\DatabaseController::class, 'store']);
|
||||
Route::post('/{database}/rotate-password', [Client\Servers\DatabaseController::class, 'rotatePassword']);
|
||||
Route::delete('/{database}', [Client\Servers\DatabaseController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/files'], function () {
|
||||
Route::get('/list', [Client\Servers\FileController::class, 'directory']);
|
||||
Route::get('/contents', [Client\Servers\FileController::class, 'contents']);
|
||||
Route::get('/exists', [Client\Servers\FileController::class, 'exists']);
|
||||
Route::get('/download', [Client\Servers\FileController::class, 'download']);
|
||||
Route::put('/rename', [Client\Servers\FileController::class, 'rename']);
|
||||
Route::post('/copy', [Client\Servers\FileController::class, 'copy']);
|
||||
Route::post('/write', [Client\Servers\FileController::class, 'write']);
|
||||
Route::post('/compress', [Client\Servers\FileController::class, 'compress']);
|
||||
Route::post('/decompress', [Client\Servers\FileController::class, 'decompress']);
|
||||
Route::post('/delete', [Client\Servers\FileController::class, 'delete']);
|
||||
Route::post('/create-folder', [Client\Servers\FileController::class, 'create']);
|
||||
Route::post('/chmod', [Client\Servers\FileController::class, 'chmod']);
|
||||
Route::post('/pull', [Client\Servers\FileController::class, 'pull'])->middleware(['throttle:10,5']);
|
||||
Route::get('/upload', Client\Servers\FileUploadController::class);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/schedules'], function () {
|
||||
Route::get('/', [Client\Servers\ScheduleController::class, 'index']);
|
||||
Route::post('/', [Client\Servers\ScheduleController::class, 'store']);
|
||||
Route::get('/{schedule}', [Client\Servers\ScheduleController::class, 'view']);
|
||||
Route::post('/{schedule}', [Client\Servers\ScheduleController::class, 'update']);
|
||||
Route::post('/{schedule}/execute', [Client\Servers\ScheduleController::class, 'execute']);
|
||||
Route::delete('/{schedule}', [Client\Servers\ScheduleController::class, 'delete']);
|
||||
|
||||
Route::post('/{schedule}/tasks', [Client\Servers\ScheduleTaskController::class, 'store']);
|
||||
Route::post('/{schedule}/tasks/{task}', [Client\Servers\ScheduleTaskController::class, 'update']);
|
||||
Route::delete('/{schedule}/tasks/{task}', [Client\Servers\ScheduleTaskController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/network'], function () {
|
||||
Route::get('/allocations', [Client\Servers\NetworkAllocationController::class, 'index']);
|
||||
Route::post('/allocations', [Client\Servers\NetworkAllocationController::class, 'store']);
|
||||
Route::post('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'update']);
|
||||
Route::post('/allocations/{allocation}/primary', [Client\Servers\NetworkAllocationController::class, 'setPrimary']);
|
||||
Route::delete('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/users'], function () {
|
||||
Route::get('/', [Client\Servers\SubuserController::class, 'index']);
|
||||
Route::post('/', [Client\Servers\SubuserController::class, 'store']);
|
||||
Route::get('/{user}', [Client\Servers\SubuserController::class, 'view']);
|
||||
Route::post('/{user}', [Client\Servers\SubuserController::class, 'update']);
|
||||
Route::delete('/{user}', [Client\Servers\SubuserController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/backups'], function () {
|
||||
Route::get('/', [Client\Servers\BackupController::class, 'index']);
|
||||
Route::post('/', [Client\Servers\BackupController::class, 'store']);
|
||||
Route::get('/{backup}', [Client\Servers\BackupController::class, 'view']);
|
||||
Route::get('/{backup}/download', [Client\Servers\BackupController::class, 'download']);
|
||||
Route::post('/{backup}/lock', [Client\Servers\BackupController::class, 'toggleLock']);
|
||||
Route::post('/{backup}/restore', [Client\Servers\BackupController::class, 'restore']);
|
||||
Route::delete('/{backup}', [Client\Servers\BackupController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/startup'], function () {
|
||||
Route::get('/', [Client\Servers\StartupController::class, 'index']);
|
||||
Route::put('/variable', [Client\Servers\StartupController::class, 'update']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/settings'], function () {
|
||||
Route::post('/rename', [Client\Servers\SettingsController::class, 'rename']);
|
||||
Route::post('/reinstall', [Client\Servers\SettingsController::class, 'reinstall']);
|
||||
Route::put('/docker-image', [Client\Servers\SettingsController::class, 'dockerImage']);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Pterodactyl\Http\Controllers\Api\Client;
|
||||
use Pterodactyl\Http\Middleware\Activity\ServerSubject;
|
||||
use Pterodactyl\Http\Middleware\Activity\AccountSubject;
|
||||
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
use Pterodactyl\Http\Middleware\Api\Client\Server\ResourceBelongsToServer;
|
||||
use Pterodactyl\Http\Middleware\Api\Client\Server\AuthenticateServerAccess;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Client Control API
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoint: /api/client
|
||||
|
|
||||
*/
|
||||
Route::get('/', [Client\ClientController::class, 'index'])->name('api:client.index');
|
||||
Route::get('/permissions', [Client\ClientController::class, 'permissions']);
|
||||
|
||||
Route::prefix('/account')->middleware(AccountSubject::class)->group(function () {
|
||||
Route::prefix('/')->withoutMiddleware(RequireTwoFactorAuthentication::class)->group(function () {
|
||||
Route::get('/', [Client\AccountController::class, 'index'])->name('api:client.account');
|
||||
Route::get('/two-factor', [Client\TwoFactorController::class, 'index']);
|
||||
Route::post('/two-factor', [Client\TwoFactorController::class, 'store']);
|
||||
Route::delete('/two-factor', [Client\TwoFactorController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::put('/email', [Client\AccountController::class, 'updateEmail'])->name('api:client.account.update-email');
|
||||
Route::put('/password', [Client\AccountController::class, 'updatePassword'])->name('api:client.account.update-password');
|
||||
|
||||
Route::get('/activity', Client\ActivityLogController::class)->name('api:client.account.activity');
|
||||
|
||||
Route::get('/api-keys', [Client\ApiKeyController::class, 'index']);
|
||||
Route::post('/api-keys', [Client\ApiKeyController::class, 'store']);
|
||||
Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']);
|
||||
|
||||
Route::prefix('/ssh-keys')->group(function () {
|
||||
Route::get('/', [Client\SSHKeyController::class, 'index']);
|
||||
Route::post('/', [Client\SSHKeyController::class, 'store']);
|
||||
Route::post('/remove', [Client\SSHKeyController::class, 'delete']);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Client Control API
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoint: /api/client/servers/{server}
|
||||
|
|
||||
*/
|
||||
Route::group([
|
||||
'prefix' => '/servers/{server}',
|
||||
'middleware' => [
|
||||
ServerSubject::class,
|
||||
AuthenticateServerAccess::class,
|
||||
ResourceBelongsToServer::class,
|
||||
],
|
||||
], function () {
|
||||
Route::get('/', [Client\Servers\ServerController::class, 'index'])->name('api:client:server.view');
|
||||
Route::get('/websocket', Client\Servers\WebsocketController::class)->name('api:client:server.ws');
|
||||
Route::get('/resources', Client\Servers\ResourceUtilizationController::class)->name('api:client:server.resources');
|
||||
Route::get('/activity', Client\Servers\ActivityLogController::class)->name('api:client:server.activity');
|
||||
|
||||
Route::post('/command', [Client\Servers\CommandController::class, 'index']);
|
||||
Route::post('/power', [Client\Servers\PowerController::class, 'index']);
|
||||
|
||||
Route::group(['prefix' => '/modpacks'], function () {
|
||||
Route::get('/', [Client\Servers\CurseForge\ModpackController::class, 'index']);
|
||||
Route::get('/{modpack}', [Client\Servers\CurseForge\ModpackController::class, 'show']);
|
||||
Route::get('/{modpack}/install', [Client\Servers\CurseForge\ModpackController::class, 'install']);
|
||||
Route::get('/{modpack}/description', [Client\Servers\CurseForge\ModpackController::class, 'description']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/databases'], function () {
|
||||
Route::get('/', [Client\Servers\DatabaseController::class, 'index']);
|
||||
Route::post('/', [Client\Servers\DatabaseController::class, 'store']);
|
||||
Route::post('/{database}/rotate-password', [Client\Servers\DatabaseController::class, 'rotatePassword']);
|
||||
Route::delete('/{database}', [Client\Servers\DatabaseController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/files'], function () {
|
||||
Route::get('/list', [Client\Servers\FileController::class, 'directory']);
|
||||
Route::get('/contents', [Client\Servers\FileController::class, 'contents']);
|
||||
Route::get('/exists', [Client\Servers\FileController::class, 'exists']);
|
||||
Route::get('/download', [Client\Servers\FileController::class, 'download']);
|
||||
Route::put('/rename', [Client\Servers\FileController::class, 'rename']);
|
||||
Route::post('/copy', [Client\Servers\FileController::class, 'copy']);
|
||||
Route::post('/write', [Client\Servers\FileController::class, 'write']);
|
||||
Route::post('/compress', [Client\Servers\FileController::class, 'compress']);
|
||||
Route::post('/decompress', [Client\Servers\FileController::class, 'decompress']);
|
||||
Route::post('/delete', [Client\Servers\FileController::class, 'delete']);
|
||||
Route::post('/create-folder', [Client\Servers\FileController::class, 'create']);
|
||||
Route::post('/chmod', [Client\Servers\FileController::class, 'chmod']);
|
||||
Route::post('/pull', [Client\Servers\FileController::class, 'pull'])->middleware(['throttle:10,5']);
|
||||
Route::get('/upload', Client\Servers\FileUploadController::class);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/schedules'], function () {
|
||||
Route::get('/', [Client\Servers\ScheduleController::class, 'index']);
|
||||
Route::post('/', [Client\Servers\ScheduleController::class, 'store']);
|
||||
Route::get('/{schedule}', [Client\Servers\ScheduleController::class, 'view']);
|
||||
Route::post('/{schedule}', [Client\Servers\ScheduleController::class, 'update']);
|
||||
Route::post('/{schedule}/execute', [Client\Servers\ScheduleController::class, 'execute']);
|
||||
Route::delete('/{schedule}', [Client\Servers\ScheduleController::class, 'delete']);
|
||||
|
||||
Route::post('/{schedule}/tasks', [Client\Servers\ScheduleTaskController::class, 'store']);
|
||||
Route::post('/{schedule}/tasks/{task}', [Client\Servers\ScheduleTaskController::class, 'update']);
|
||||
Route::delete('/{schedule}/tasks/{task}', [Client\Servers\ScheduleTaskController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/network'], function () {
|
||||
Route::get('/allocations', [Client\Servers\NetworkAllocationController::class, 'index']);
|
||||
Route::post('/allocations', [Client\Servers\NetworkAllocationController::class, 'store']);
|
||||
Route::post('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'update']);
|
||||
Route::post('/allocations/{allocation}/primary', [Client\Servers\NetworkAllocationController::class, 'setPrimary']);
|
||||
Route::delete('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/users'], function () {
|
||||
Route::get('/', [Client\Servers\SubuserController::class, 'index']);
|
||||
Route::post('/', [Client\Servers\SubuserController::class, 'store']);
|
||||
Route::get('/{user}', [Client\Servers\SubuserController::class, 'view']);
|
||||
Route::post('/{user}', [Client\Servers\SubuserController::class, 'update']);
|
||||
Route::delete('/{user}', [Client\Servers\SubuserController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/backups'], function () {
|
||||
Route::get('/', [Client\Servers\BackupController::class, 'index']);
|
||||
Route::post('/', [Client\Servers\BackupController::class, 'store']);
|
||||
Route::get('/{backup}', [Client\Servers\BackupController::class, 'view']);
|
||||
Route::get('/{backup}/download', [Client\Servers\BackupController::class, 'download']);
|
||||
Route::post('/{backup}/lock', [Client\Servers\BackupController::class, 'toggleLock']);
|
||||
Route::post('/{backup}/restore', [Client\Servers\BackupController::class, 'restore']);
|
||||
Route::delete('/{backup}', [Client\Servers\BackupController::class, 'delete']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/startup'], function () {
|
||||
Route::get('/', [Client\Servers\StartupController::class, 'index']);
|
||||
Route::put('/variable', [Client\Servers\StartupController::class, 'update']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '/settings'], function () {
|
||||
Route::post('/rename', [Client\Servers\SettingsController::class, 'rename']);
|
||||
Route::post('/reinstall', [Client\Servers\SettingsController::class, 'reinstall']);
|
||||
Route::put('/docker-image', [Client\Servers\SettingsController::class, 'dockerImage']);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue