Merge branch 'develop' into develop

This commit is contained in:
Dane Everitt 2020-10-31 13:47:12 -07:00 committed by GitHub
commit 665a4dd8a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
115 changed files with 3434 additions and 1970 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
public
resources/views

View File

@ -1,14 +0,0 @@
---
name: "⛔ Installation Help"
about: 'Visit our Discord for installation help: https://pterodactyl.io/discord'
---
We use GitHub issues only to discuss about Pterodactyl bugs and new features. For
this kind of questions about using Pterodactyl, please visit our Discord for assistance: https://pterodactyl.io/discord
DO NOT REPORT ISSUES CONFIGURING: SSL, PHP, APACHE, NGINX, YOUR MACHINE, SSH, SFTP, ETC. ON THIS GITHUB TRACKER.
For assistance installating this software, as well as debugging issues with dependencies, please use our discord server: https://discord.gg/pterodactyl
PLEASE stop spamming our tracker with "bugs" that are not related to this project.

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: 🤷 Installation Help
url: https://discord.gg/pterodactyl
about: Please visit our Discord for help with your installation.
- name: ❓ General Question
url: https://discord.gg/pterodactyl
about: Please visit our Discord for general questions about Pterodactyl.

View File

@ -3,6 +3,56 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines. This project follows [Semantic Versioning](http://semver.org) guidelines.
## v1.0.3
### Fixed
* Fixes bug causing subusers to not be creatable or editable via the frontend for servers.
* Fixes system timezone not being passed along properly to the MySQL connection causing scheduled tasks to run every minute when the MySQL instance and Panel timezone did not line up.
* Fixes listing servers owned by a user in the admin area to actually list their servers.
### Changed
* Adds SameSite `lax` attribute for cookies generated by the Panel.
* Adds better filtering for searching servers in the admin area to better key off name, uuid, or owner username/email.
## v1.0.2
### Added
* Adds support for searching inside the file editor.
* Adds support for manually executing a schedule regardless of if it is currently queued or not.
* Adds an indicator to the schedule UI to show when a schedule is currently processing.
* Adds support for setting the `backup_limit` of a server via the API.
* **[Security]** Adds login throttling to the 2FA verification endpoint.
### Fixed
* Fixes subuser page title missing server name.
* Fixes schedule task `sequence_id` not properly being reset when a schedule's task is deleted.
* Fixes misc. UI bugs throughout the frontend when long text overflows its bounds.
* Fixes user deletion command to properly handle email & ID searching.
* Fixes color issues in the terminal causing certain text & background combinations to be illegible.
* Fixes reCAPTCHA not properly resetting on login failure.
* Fixes error messages not properly resetting between login screens.
* Fixes a UI crash when attempting to create or view a directory or file that contained the `%` somewhere in the name.
### Changed
* Updated the search modal to close itself when the ESC key is pressed.
* Updates the schedule view and editing UI to better display information to users.
* Changed logic powering server searching on the frontend to return more accurate results and return all servers when executing the query as an admin.
* Admin CP link no longer opens in a new tab.
* Mounts will no longer allow a user to mount certain directory combinations. This blocks mounting one server's files into another server, and blocks using the server data directory as a mount destination.
* Cleaned up assorted server build modification code.
* Updates default eggs to have improved install scripts and more consistent container usage.
## v1.0.1
### Fixed
* Fixes 500 error when mounting a mount to a server, and other related errors when handling mounts.
* Ensures that `server_transfers` database is deleted if it already exists to avoid unnecessary error.
* Fixes servers getting marked as "not installed" when modifying their startup arguments.
* Fixes filemanager breadcrumbs being set incorrectly when navigating between files and folders.
### Changed
* Change the requests per minute from 240 to 720 for the client API to avoid unecessarily displaying
"Too Many Requests" errors.
* Added error output to certain commands that will output and terminate the command execution if the database
migrations have not been run correctly for the instance.
## v1.0.0 ## v1.0.0
Pterodactyl 1.0 represents the culmination of over two years of work, almost 2,000 commits, endless bug and feature requests, and a dream that Pterodactyl 1.0 represents the culmination of over two years of work, almost 2,000 commits, endless bug and feature requests, and a dream that
has been in the making since 2013. 🎉 has been in the making since 2013. 🎉

View File

@ -3,7 +3,6 @@
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pterodactyl/panel/tests?label=Tests&style=for-the-badge) ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pterodactyl/panel/tests?label=Tests&style=for-the-badge)
![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white&style=for-the-badge) ![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white&style=for-the-badge)
![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/panel/latest/total?style=for-the-badge) ![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/panel/latest/total?style=for-the-badge)
![GitHub Pre-Releases](https://img.shields.io/github/downloads-pre/pterodactyl/panel/v1.0.0-rc.7/total?style=for-the-badge)
![GitHub contributors](https://img.shields.io/github/contributors/pterodactyl/panel?style=for-the-badge) ![GitHub contributors](https://img.shields.io/github/contributors/pterodactyl/panel?style=for-the-badge)
# Pterodactyl Panel # Pterodactyl Panel
@ -16,18 +15,21 @@ Stop settling for less. Make game servers a first class citizen on your platform
![Image](https://cdn.pterodactyl.io/site-assets/pterodactyl_v1_demo.gif) ![Image](https://cdn.pterodactyl.io/site-assets/pterodactyl_v1_demo.gif)
## Sponsors ## Sponsors
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement. I would like to extend my sincere thanks to the following sponsors for helping fund Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/DaneEveritt) [Interested in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
| Company | About | | Company | About |
| ------- | ----- | | ------- | ----- |
| [**WISP**](https://wisp.gg) | Extra features. |
| [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. | | [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. |
| [**VersatileNode**](https://versatilenode.com/) | Looking to host a minecraft server, vps, or a website? VersatileNode is one of the most affordable hosting providers to provide quality yet cheap services with incredible support. |
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. | | [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. | | [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! | | [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
| [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. | | [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
| [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. | | [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. |
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims to cheap services on quality servers. Premium i9-9900K processors will run your game like a dream. |
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
| [**HostBend**](https://hostbend.com/) | HostBend offers a variety of solutions for developers, students, and others who have a tight budget but don't want to compromise quality and support. |
## Documentation ## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html) * [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)

View File

@ -144,6 +144,11 @@ class AppSettingsCommand extends Command
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(trans('command/messages.environment.app.settings'), true) ? 'false' : 'true'; $this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(trans('command/messages.environment.app.settings'), true) ? 'false' : 'true';
} }
// Make sure session cookies are set as "secure" when using HTTPS
if (strpos($this->variables['APP_URL'], 'https://') === 0) {
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
}
$this->checkForRedis(); $this->checkForRedis();
$this->writeToEnvironment($this->variables); $this->writeToEnvironment($this->variables);

View File

@ -1,62 +1,37 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Console\Commands\Schedule; namespace Pterodactyl\Console\Commands\Schedule;
use Cake\Chronos\Chronos; use Throwable;
use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Collection; use Pterodactyl\Models\Schedule;
use Illuminate\Support\Facades\Log;
use Pterodactyl\Services\Schedules\ProcessScheduleService; use Pterodactyl\Services\Schedules\ProcessScheduleService;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class ProcessRunnableCommand extends Command class ProcessRunnableCommand extends Command
{ {
/** /**
* @var string * @var string
*/ */
protected $description = 'Process schedules in the database and determine which are ready to run.'; protected $signature = 'p:schedule:process';
/**
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
*/
protected $processScheduleService;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/
protected $repository;
/** /**
* @var string * @var string
*/ */
protected $signature = 'p:schedule:process'; protected $description = 'Process schedules in the database and determine which are ready to run.';
/**
* ProcessRunnableCommand constructor.
*
* @param \Pterodactyl\Services\Schedules\ProcessScheduleService $processScheduleService
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
*/
public function __construct(ProcessScheduleService $processScheduleService, ScheduleRepositoryInterface $repository)
{
parent::__construct();
$this->processScheduleService = $processScheduleService;
$this->repository = $repository;
}
/** /**
* Handle command execution. * Handle command execution.
*/ */
public function handle() public function handle()
{ {
$schedules = $this->repository->getSchedulesToProcess(Chronos::now()->toAtomString()); $schedules = Schedule::query()->with('tasks')
->where('is_active', true)
->where('is_processing', false)
->whereRaw('next_run_at <= NOW()')
->get();
if ($schedules->count() < 1) { if ($schedules->count() < 1) {
$this->line('There are no scheduled tasks for servers that need to be run.'); $this->line('There are no scheduled tasks for servers that need to be run.');
@ -64,23 +39,41 @@ class ProcessRunnableCommand extends Command
} }
$bar = $this->output->createProgressBar(count($schedules)); $bar = $this->output->createProgressBar(count($schedules));
$schedules->each(function ($schedule) use ($bar) { foreach ($schedules as $schedule) {
if ($schedule->tasks instanceof Collection && count($schedule->tasks) > 0) {
$this->processScheduleService->handle($schedule);
if ($this->input->isInteractive()) {
$bar->clear(); $bar->clear();
$this->processSchedule($schedule);
$bar->advance();
$bar->display();
}
$this->line('');
}
/**
* Processes a given schedule and logs and errors encountered the console output. This should
* never throw an exception out, otherwise you'll end up killing the entire run group causing
* any other schedules to not process correctly.
*
* @param \Pterodactyl\Models\Schedule $schedule
* @see https://github.com/pterodactyl/panel/issues/2609
*/
protected function processSchedule(Schedule $schedule)
{
if ($schedule->tasks->isEmpty()) {
return;
}
try {
$this->getLaravel()->make(ProcessScheduleService::class)->handle($schedule);
$this->line(trans('command/messages.schedule.output_line', [ $this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name, 'schedule' => $schedule->name,
'hash' => $schedule->hashid, 'hash' => $schedule->hashid,
])); ]));
} } catch (Throwable | Exception $exception) {
} Log::error($exception, ['schedule_id' => $schedule->id]);
$bar->advance(); $this->error("An error was encountered while processing Schedule #{$schedule->id}: " . $exception->getMessage());
$bar->display(); }
});
$this->line('');
} }
} }

View File

@ -58,8 +58,9 @@ class DeleteUserCommand extends Command
Assert::notEmpty($search, 'Search term should be an email address, got: %s.'); Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
$results = User::query() $results = User::query()
->where('email', 'LIKE', "$search%") ->where('id', 'LIKE', "$search%")
->where('username', 'LIKE', "$search%") ->orWhere('username', 'LIKE', "$search%")
->orWhere('email', 'LIKE', "$search%")
->get(); ->get();
if (count($results) < 1) { if (count($results) < 1) {

View File

@ -15,16 +15,6 @@ interface ScheduleRepositoryInterface extends RepositoryInterface
*/ */
public function findServerSchedules(int $server): Collection; public function findServerSchedules(int $server): Collection;
/**
* Load the tasks relationship onto the Schedule module if they are not
* already present.
*
* @param \Pterodactyl\Models\Schedule $schedule
* @param bool $refresh
* @return \Pterodactyl\Models\Schedule
*/
public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule;
/** /**
* Return a schedule model with all of the associated tasks as a relationship. * Return a schedule model with all of the associated tasks as a relationship.
* *
@ -34,12 +24,4 @@ interface ScheduleRepositoryInterface extends RepositoryInterface
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function getScheduleWithTasks(int $schedule): Schedule; public function getScheduleWithTasks(int $schedule): Schedule;
/**
* Return all of the schedules that should be processed.
*
* @param string $timestamp
* @return \Illuminate\Support\Collection
*/
public function getSchedulesToProcess(string $timestamp): Collection;
} }

View File

@ -139,16 +139,4 @@ interface ServerRepositoryInterface extends RepositoryInterface
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/ */
public function loadAllServersForNode(int $node, int $limit): LengthAwarePaginator; public function loadAllServersForNode(int $node, int $limit): LengthAwarePaginator;
/**
* Returns every server that exists for a given node.
*
* This is different from {@see loadAllServersForNode} because
* it does not paginate the response.
*
* @param int $node
*
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public function loadEveryServerForNode(int $node);
} }

25
app/Helpers/Time.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace Pterodactyl\Helpers;
use Carbon\CarbonImmutable;
final class Time
{
/**
* Gets the time offset from the provided timezone relative to UTC as a number. This
* is used in the database configuration since we can't always rely on there being support
* for named timezones in MySQL.
*
* Returns the timezone as a string like +08:00 or -05:00 depending on the app timezone.
*
* @param string $timezone
* @return string
*/
public static function getMySQLTimezoneOffset(string $timezone): string
{
$offset = round(CarbonImmutable::now($timezone)->getTimezone()->getOffset(CarbonImmutable::now('UTC')) / 3600);
return sprintf('%s%s:00', $offset > 0 ? '+' : '-', str_pad(abs($offset), 2, '0', STR_PAD_LEFT));
}
}

View File

@ -52,7 +52,12 @@ class Utilities
)->getNextRunDate()); )->getNextRunDate());
} }
public static function checked($name, $default) /**
* @param string $name
* @param mixed $default
* @return string
*/
public static function checked(string $name, $default)
{ {
$errors = session('errors'); $errors = session('errors');

View File

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Illuminate\Support\Str;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pterodactyl\Models\Nest; use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Mount; use Pterodactyl\Models\Mount;
@ -101,7 +102,6 @@ class MountController extends Controller
*/ */
public function create(MountFormRequest $request) public function create(MountFormRequest $request)
{ {
/** @var \Pterodactyl\Models\Mount $mount */
$model = (new Mount())->fill($request->validated()); $model = (new Mount())->fill($request->validated());
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]); $model->forceFill(['uuid' => Uuid::uuid4()->toString()]);

View File

@ -6,7 +6,9 @@ use Illuminate\Http\Request;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\QueryBuilder;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Spatie\QueryBuilder\AllowedFilter;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Models\Filters\AdminServerFilter;
use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository;
class ServerController extends Controller class ServerController extends Controller
@ -45,8 +47,10 @@ class ServerController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation')) $servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
->allowedFilters(['uuid', 'name', 'image']) ->allowedFilters([
->allowedSorts(['id', 'uuid']) AllowedFilter::exact('owner_id'),
AllowedFilter::custom('*', new AdminServerFilter),
])
->paginate(config()->get('pterodactyl.paginate.admin.servers')); ->paginate(config()->get('pterodactyl.paginate.admin.servers'));
return $this->view->make('admin.servers.index', ['servers' => $servers]); return $this->view->make('admin.servers.index', ['servers' => $servers]);

View File

@ -5,6 +5,8 @@ namespace Pterodactyl\Http\Controllers\Api\Client;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\Permission; use Pterodactyl\Models\Permission;
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Pterodactyl\Models\Filters\MultiFieldServerFilter;
use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Transformers\Api\Client\ServerTransformer; use Pterodactyl\Transformers\Api\Client\ServerTransformer;
use Pterodactyl\Http\Requests\Api\Client\GetServersRequest; use Pterodactyl\Http\Requests\Api\Client\GetServersRequest;
@ -43,21 +45,32 @@ class ClientController extends ClientApiController
// Start the query builder and ensure we eager load any requested relationships from the request. // Start the query builder and ensure we eager load any requested relationships from the request.
$builder = QueryBuilder::for( $builder = QueryBuilder::for(
Server::query()->with($this->getIncludesForTransformer($transformer, ['node'])) Server::query()->with($this->getIncludesForTransformer($transformer, ['node']))
)->allowedFilters('uuid', 'name', 'external_id'); )->allowedFilters([
'uuid',
'name',
'external_id',
AllowedFilter::custom('*', new MultiFieldServerFilter),
]);
$type = $request->input('type');
// Either return all of the servers the user has access to because they are an admin `?type=admin` or // Either return all of the servers the user has access to because they are an admin `?type=admin` or
// just return all of the servers the user has access to because they are the owner or a subuser of the // just return all of the servers the user has access to because they are the owner or a subuser of the
// server. // server. If ?type=admin-all is passed all servers on the system will be returned to the user, rather
if ($request->input('type') === 'admin') { // than only servers they can see because they are an admin.
$builder = $user->root_admin if (in_array($type, ['admin', 'admin-all'])) {
? $builder->whereNotIn('id', $user->accessibleServers()->pluck('id')->all())
// If they aren't an admin but want all the admin servers don't fail the request, just // If they aren't an admin but want all the admin servers don't fail the request, just
// make it a query that will never return any results back. // make it a query that will never return any results back.
: $builder->whereRaw('1 = 2'); if (! $user->root_admin) {
} elseif ($request->input('type') === 'owner') { $builder->whereRaw('1 = 2');
$builder = $builder->where('owner_id', $user->id);
} else { } else {
$builder = $builder->whereIn('id', $user->accessibleServers()->pluck('id')->all()); $builder = $type === 'admin-all'
? $builder
: $builder->whereNotIn('servers.id', $user->accessibleServers()->pluck('id')->all());
}
} else if ($type === 'owner') {
$builder = $builder->where('servers.owner_id', $user->id);
} else {
$builder = $builder->whereIn('servers.id', $user->accessibleServers()->pluck('id')->all());
} }
$servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query()); $servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query());

View File

@ -10,15 +10,19 @@ use Pterodactyl\Models\Server;
use Pterodactyl\Models\Schedule; use Pterodactyl\Models\Schedule;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Pterodactyl\Helpers\Utilities; use Pterodactyl\Helpers\Utilities;
use Pterodactyl\Jobs\Schedule\RunTaskJob;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Repositories\Eloquent\ScheduleRepository; use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
use Pterodactyl\Services\Schedules\ProcessScheduleService;
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer; use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\UpdateScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\UpdateScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest;
class ScheduleController extends ClientApiController class ScheduleController extends ClientApiController
{ {
@ -27,16 +31,23 @@ class ScheduleController extends ClientApiController
*/ */
private $repository; private $repository;
/**
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
*/
private $service;
/** /**
* ScheduleController constructor. * ScheduleController constructor.
* *
* @param \Pterodactyl\Repositories\Eloquent\ScheduleRepository $repository * @param \Pterodactyl\Repositories\Eloquent\ScheduleRepository $repository
* @param \Pterodactyl\Services\Schedules\ProcessScheduleService $service
*/ */
public function __construct(ScheduleRepository $repository) public function __construct(ScheduleRepository $repository, ProcessScheduleService $service)
{ {
parent::__construct(); parent::__construct();
$this->repository = $repository; $this->repository = $repository;
$this->service = $service;
} }
/** /**
@ -147,6 +158,30 @@ class ScheduleController extends ClientApiController
->toArray(); ->toArray();
} }
/**
* Executes a given schedule immediately rather than waiting on it's normally scheduled time
* to pass. This does not care about the schedule state.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest $request
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Schedule $schedule
* @return \Illuminate\Http\JsonResponse
*
* @throws \Throwable
*/
public function execute(TriggerScheduleRequest $request, Server $server, Schedule $schedule)
{
if (!$schedule->is_active) {
throw new BadRequestHttpException(
'Cannot trigger schedule exection for a schedule that is not currently active.'
);
}
$this->service->handle($schedule, true);
return new JsonResponse([], JsonResponse::HTTP_ACCEPTED);
}
/** /**
* Deletes a schedule and it's associated tasks. * Deletes a schedule and it's associated tasks.
* *

View File

@ -56,7 +56,8 @@ class ScheduleTaskController extends ClientApiController
); );
} }
$lastTask = $schedule->tasks->last(); /** @var \Pterodactyl\Models\Task|null $lastTask */
$lastTask = $schedule->tasks()->orderByDesc('sequence_id')->first();
/** @var \Pterodactyl\Models\Task $task */ /** @var \Pterodactyl\Models\Task $task */
$task = $this->repository->create([ $task = $this->repository->create([
@ -102,13 +103,16 @@ class ScheduleTaskController extends ClientApiController
} }
/** /**
* Determines if a user can delete the task for a given server. * Delete a given task for a schedule. If there are subsequent tasks stored in the database
* for this schedule their sequence IDs are decremented properly.
* *
* @param \Pterodactyl\Http\Requests\Api\Client\ClientApiRequest $request * @param \Pterodactyl\Http\Requests\Api\Client\ClientApiRequest $request
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Schedule $schedule * @param \Pterodactyl\Models\Schedule $schedule
* @param \Pterodactyl\Models\Task $task * @param \Pterodactyl\Models\Task $task
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*
* @throws \Exception
*/ */
public function delete(ClientApiRequest $request, Server $server, Schedule $schedule, Task $task) public function delete(ClientApiRequest $request, Server $server, Schedule $schedule, Task $task)
{ {
@ -120,8 +124,12 @@ class ScheduleTaskController extends ClientApiController
throw new HttpForbiddenException('You do not have permission to perform this action.'); throw new HttpForbiddenException('You do not have permission to perform this action.');
} }
$this->repository->delete($task->id); $schedule->tasks()->where('sequence_id', '>', $task->sequence_id)->update([
'sequence_id' => $schedule->tasks()->getConnection()->raw('(sequence_id - 1)'),
]);
return JsonResponse::create(null, Response::HTTP_NO_CONTENT); $task->delete();
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
} }
} }

View File

@ -3,11 +3,14 @@
namespace Pterodactyl\Http\Controllers\Api\Remote\Servers; namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Services\Eggs\EggConfigurationService; use Pterodactyl\Services\Eggs\EggConfigurationService;
use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Http\Resources\Wings\ServerConfigurationCollection;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService; use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
class ServerDetailsController extends Controller class ServerDetailsController extends Controller
@ -27,11 +30,6 @@ class ServerDetailsController extends Controller
*/ */
private $configurationStructureService; private $configurationStructureService;
/**
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
*/
private $nodeRepository;
/** /**
* ServerConfigurationController constructor. * ServerConfigurationController constructor.
* *
@ -49,7 +47,6 @@ class ServerDetailsController extends Controller
$this->eggConfigurationService = $eggConfigurationService; $this->eggConfigurationService = $eggConfigurationService;
$this->repository = $repository; $this->repository = $repository;
$this->configurationStructureService = $configurationStructureService; $this->configurationStructureService = $configurationStructureService;
$this->nodeRepository = $nodeRepository;
} }
/** /**
@ -66,7 +63,7 @@ class ServerDetailsController extends Controller
{ {
$server = $this->repository->getByUuid($uuid); $server = $this->repository->getByUuid($uuid);
return JsonResponse::create([ return new JsonResponse([
'settings' => $this->configurationStructureService->handle($server), 'settings' => $this->configurationStructureService->handle($server),
'process_configuration' => $this->eggConfigurationService->handle($server), 'process_configuration' => $this->eggConfigurationService->handle($server),
]); ]);
@ -76,25 +73,19 @@ class ServerDetailsController extends Controller
* Lists all servers with their configurations that are assigned to the requesting node. * Lists all servers with their configurations that are assigned to the requesting node.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* * @return \Pterodactyl\Http\Resources\Wings\ServerConfigurationCollection
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function list(Request $request) public function list(Request $request)
{ {
/** @var \Pterodactyl\Models\Node $node */
$node = $request->attributes->get('node'); $node = $request->attributes->get('node');
$servers = $this->repository->loadEveryServerForNode($node->id);
$configurations = []; // Avoid run-away N+1 SQL queries by pre-loading the relationships that are used
// within each of the services called below.
$servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables', 'location')
->where('node_id', $node->id)
->paginate($request->input('per_page', 50));
foreach ($servers as $server) { return new ServerConfigurationCollection($servers);
$configurations[$server->uuid] = [
'settings' => $this->configurationStructureService->handle($server),
'process_configuration' => $this->eggConfigurationService->handle($server),
];
}
return JsonResponse::create($configurations);
} }
} }

View File

@ -2,11 +2,13 @@
namespace Pterodactyl\Http\Controllers\Auth; namespace Pterodactyl\Http\Controllers\Auth;
use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager; use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use PragmaRX\Google2FA\Google2FA; use PragmaRX\Google2FA\Google2FA;
use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest; use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest;
use Illuminate\Contracts\Cache\Repository as CacheRepository; use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
@ -80,30 +82,32 @@ class LoginCheckpointController extends AbstractLoginController
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Exception
* @throws \Illuminate\Validation\ValidationException
*/ */
public function __invoke(LoginCheckpointRequest $request): JsonResponse public function __invoke(LoginCheckpointRequest $request): JsonResponse
{ {
$token = $request->input('confirmation_token'); if ($this->hasTooManyLoginAttempts($request)) {
$recoveryToken = $request->input('recovery_token'); $this->sendLockoutResponse($request);
}
$token = $request->input('confirmation_token');
try { try {
/** @var \Pterodactyl\Models\User $user */ /** @var \Pterodactyl\Models\User $user */
$user = $this->repository->find($this->cache->get($token, 0)); $user = User::query()->findOrFail($this->cache->get($token, 0));
} catch (RecordNotFoundException $exception) { } catch (ModelNotFoundException $exception) {
return $this->sendFailedLoginResponse($request, null, 'The authentication token provided has expired, please refresh the page and try again.'); $this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse(
$request, null, 'The authentication token provided has expired, please refresh the page and try again.'
);
} }
// If we got a recovery token try to find one that matches for the user and then continue // Recovery tokens go through a slightly different pathway for usage.
// through the process (and delete the token). if (! is_null($recoveryToken = $request->input('recovery_token'))) {
if (! is_null($recoveryToken)) { if ($this->isValidRecoveryToken($user, $recoveryToken)) {
foreach ($user->recoveryTokens as $token) {
if (password_verify($recoveryToken, $token->token)) {
$this->recoveryTokenRepository->delete($token->id);
return $this->sendLoginResponse($user, $request); return $this->sendLoginResponse($user, $request);
} }
}
} else { } else {
$decrypted = $this->encrypter->decrypt($user->totp_secret); $decrypted = $this->encrypter->decrypt($user->totp_secret);
@ -114,6 +118,31 @@ class LoginCheckpointController extends AbstractLoginController
} }
} }
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request, $user, ! empty($recoveryToken) ? 'The recovery token provided is not valid.' : null); return $this->sendFailedLoginResponse($request, $user, ! empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
} }
/**
* Determines if a given recovery token is valid for the user account. If we find a matching token
* it will be deleted from the database.
*
* @param \Pterodactyl\Models\User $user
* @param string $value
* @return bool
*
* @throws \Exception
*/
protected function isValidRecoveryToken(User $user, string $value)
{
foreach ($user->recoveryTokens as $token) {
if (password_verify($value, $token->token)) {
$token->delete();
return true;
}
}
return false;
}
} }

View File

@ -69,7 +69,7 @@ class DaemonAuthenticate
// Ensure that all of the correct parts are provided in the header. // Ensure that all of the correct parts are provided in the header.
if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) { if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) {
throw new BadRequestHttpException( throw new BadRequestHttpException(
'The Authorization headed provided was not in a valid format.' 'The Authorization header provided was not in a valid format.'
); );
} }

View File

@ -55,6 +55,7 @@ class StoreServerRequest extends ApplicationApiRequest
'feature_limits' => 'required|array', 'feature_limits' => 'required|array',
'feature_limits.databases' => $rules['database_limit'], 'feature_limits.databases' => $rules['database_limit'],
'feature_limits.allocations' => $rules['allocation_limit'], 'feature_limits.allocations' => $rules['allocation_limit'],
'feature_limits.backups' => $rules['backup_limit'],
// Placeholders for rules added in withValidator() function. // Placeholders for rules added in withValidator() function.
'allocation.default' => '', 'allocation.default' => '',
@ -102,6 +103,7 @@ class StoreServerRequest extends ApplicationApiRequest
'start_on_completion' => array_get($data, 'start_on_completion', false), 'start_on_completion' => array_get($data, 'start_on_completion', false),
'database_limit' => array_get($data, 'feature_limits.databases'), 'database_limit' => array_get($data, 'feature_limits.databases'),
'allocation_limit' => array_get($data, 'feature_limits.allocations'), 'allocation_limit' => array_get($data, 'feature_limits.allocations'),
'backup_limit' => array_get($data, 'feature_limits.backups'),
]; ];
} }

View File

@ -47,6 +47,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
'feature_limits' => 'required|array', 'feature_limits' => 'required|array',
'feature_limits.databases' => $rules['database_limit'], 'feature_limits.databases' => $rules['database_limit'],
'feature_limits.allocations' => $rules['allocation_limit'], 'feature_limits.allocations' => $rules['allocation_limit'],
'feature_limits.backups' => $rules['backup_limit'],
]; ];
} }
@ -60,8 +61,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
$data = parent::validated(); $data = parent::validated();
$data['allocation_id'] = $data['allocation']; $data['allocation_id'] = $data['allocation'];
$data['database_limit'] = $data['feature_limits']['databases']; $data['database_limit'] = $data['feature_limits']['databases'] ?? null;
$data['allocation_limit'] = $data['feature_limits']['allocations']; $data['allocation_limit'] = $data['feature_limits']['allocations'] ?? null;
$data['backup_limit'] = $data['feature_limits']['backups'] ?? null;
unset($data['allocation'], $data['feature_limits']); unset($data['allocation'], $data['feature_limits']);
// Adjust the limits field to match what is expected by the model. // Adjust the limits field to match what is expected by the model.
@ -90,6 +92,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
'remove_allocations.*' => 'allocation to remove', 'remove_allocations.*' => 'allocation to remove',
'feature_limits.databases' => 'Database Limit', 'feature_limits.databases' => 'Database Limit',
'feature_limits.allocations' => 'Allocation Limit', 'feature_limits.allocations' => 'Allocation Limit',
'feature_limits.backups' => 'Backup Limit',
]; ];
} }

View File

@ -0,0 +1,25 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
use Pterodactyl\Models\Permission;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class TriggerScheduleRequest extends ClientApiRequest
{
/**
* @return string
*/
public function permission(): string
{
return Permission::ACTION_SCHEDULE_UPDATE;
}
/**
* @return array
*/
public function rules(): array
{
return [];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Pterodactyl\Http\Resources\Wings;
use Pterodactyl\Models\Server;
use Illuminate\Container\Container;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Pterodactyl\Services\Eggs\EggConfigurationService;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
class ServerConfigurationCollection extends ResourceCollection
{
/**
* Converts a collection of Server models into an array of configuration responses
* that can be understood by Wings. Make sure you've properly loaded the required
* relationships on the Server models before calling this function, otherwise you'll
* have some serious performance issues from all of the N+1 queries.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
$egg = Container::getInstance()->make(EggConfigurationService::class);
$configuration = Container::getInstance()->make(ServerConfigurationStructureService::class);
return $this->collection->map(function (Server $server) use ($configuration, $egg) {
return [
'uuid' => $server->uuid,
'settings' => $configuration->handle($server),
'process_configuration' => $egg->handle($server),
];
})->toArray();
}
}

View File

@ -7,11 +7,12 @@ use Pterodactyl\Jobs\Job;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Pterodactyl\Models\Task; use Pterodactyl\Models\Task;
use InvalidArgumentException; use InvalidArgumentException;
use Pterodactyl\Models\Schedule;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Services\Backups\InitiateBackupService; use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository; use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository; use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
@ -42,15 +43,13 @@ class RunTaskJob extends Job implements ShouldQueue
* @param \Pterodactyl\Repositories\Wings\DaemonCommandRepository $commandRepository * @param \Pterodactyl\Repositories\Wings\DaemonCommandRepository $commandRepository
* @param \Pterodactyl\Services\Backups\InitiateBackupService $backupService * @param \Pterodactyl\Services\Backups\InitiateBackupService $backupService
* @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository * @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository
* *
* @throws \Throwable * @throws \Throwable
*/ */
public function handle( public function handle(
DaemonCommandRepository $commandRepository, DaemonCommandRepository $commandRepository,
InitiateBackupService $backupService, InitiateBackupService $backupService,
DaemonPowerRepository $powerRepository, DaemonPowerRepository $powerRepository
TaskRepository $taskRepository
) { ) {
// Do not process a task that is not set to active. // Do not process a task that is not set to active.
if (! $this->task->schedule->is_active) { if (! $this->task->schedule->is_active) {

View File

@ -0,0 +1,40 @@
<?php
namespace Pterodactyl\Models\Filters;
use BadMethodCallException;
use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
class AdminServerFilter implements Filter
{
/**
* A multi-column filter for the servers table that allows an administrative user to search
* across UUID, name, owner username, and owner email.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $value
* @param string $property
*/
public function __invoke(Builder $query, $value, string $property)
{
if ($query->getQuery()->from !== 'servers') {
throw new BadMethodCallException(
'Cannot use the AdminServerFilter against a non-server model.'
);
}
$query
->select('servers.*')
->leftJoin('users', 'users.id', '=', 'servers.owner_id')
->where(function (Builder $builder) use ($value) {
$builder->where('servers.uuid', $value)
->orWhere('servers.uuid', 'LIKE', "$value%")
->orWhere('servers.uuidShort', $value)
->orWhere('servers.external_id', $value)
->orWhereRaw('LOWER(users.username) LIKE ?', ["%$value%"])
->orWhereRaw('LOWER(users.email) LIKE ?', ["$value%"])
->orWhereRaw('LOWER(servers.name) LIKE ?', ["%$value%"]);
})
->groupBy('servers.id');
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Pterodactyl\Models\Filters;
use BadMethodCallException;
use Illuminate\Support\Str;
use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
class MultiFieldServerFilter implements Filter
{
/**
* If we detect that the value matches an IPv4 address we will use a different type of filtering
* to look at the allocations.
*/
private const IPV4_REGEX = '/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(\:\d{1,5})?$/';
/**
* A multi-column filter for the servers table that allows you to pass in a single value and
* search across multiple columns. This allows us to provide a very generic search ability for
* the frontend.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $value
* @param string $property
*/
public function __invoke(Builder $query, $value, string $property)
{
if ($query->getQuery()->from !== 'servers') {
throw new BadMethodCallException(
'Cannot use the MultiFieldServerFilter against a non-server model.'
);
}
if (preg_match(self::IPV4_REGEX, $value) || preg_match('/^:\d{1,5}$/', $value)) {
$query
// Only select the server values, otherwise you'll end up merging the allocation and
// server objects together, resulting in incorrect behavior and returned values.
->select('servers.*')
->join('allocations', 'allocations.server_id', '=', 'servers.id')
->where(function (Builder $builder) use ($value) {
$parts = explode(':', $value);
$builder->when(
!Str::startsWith($value, ':'),
// When the string does not start with a ":" it means we're looking for an IP or IP:Port
// combo, so use a query to handle that.
function (Builder $builder) use ($parts) {
$builder->orWhere('allocations.ip', $parts[0]);
if (!is_null($parts[1] ?? null)) {
$builder->where('allocations.port', 'LIKE', "{$parts[1]}%");
}
},
// Otherwise, just try to search for that specific port in the allocations.
function (Builder $builder) use ($value) {
$builder->orWhere('allocations.port', 'LIKE', substr($value, 1) . '%');
}
);
})
->groupBy('servers.id');
return;
}
$query
->where(function (Builder $builder) use ($value) {
$builder->where('servers.uuid', $value)
->orWhere('servers.uuid', 'LIKE', "$value%")
->orWhere('servers.uuidShort', $value)
->orWhere('servers.external_id', $value)
->orWhereRaw('LOWER(servers.name) LIKE ?', ["%$value%"]);
});
}
}

View File

@ -2,6 +2,8 @@
namespace Pterodactyl\Models; namespace Pterodactyl\Models;
use Illuminate\Validation\Rules\NotIn;
/** /**
* @property int $id * @property int $id
* @property string $uuid * @property string $uuid
@ -63,6 +65,20 @@ class Mount extends Model
'user_mountable' => 'sometimes|boolean', 'user_mountable' => 'sometimes|boolean',
]; ];
/**
* Implement language verification by overriding Eloquence's gather
* rules function.
*/
public static function getRules()
{
$rules = parent::getRules();
$rules['source'][] = new NotIn(Mount::$invalidSourcePaths);
$rules['target'][] = new NotIn(Mount::$invalidTargetPaths);
return $rules;
}
/** /**
* Disable timestamps on this model. * Disable timestamps on this model.
* *
@ -70,6 +86,26 @@ class Mount extends Model
*/ */
public $timestamps = false; public $timestamps = false;
/**
* Blacklisted source paths
*
* @var string[]
*/
public static $invalidSourcePaths = [
'/etc/pterodactyl',
'/var/lib/pterodactyl/volumes',
'/srv/daemon-data',
];
/**
* Blacklisted target paths
*
* @var string[]
*/
public static $invalidTargetPaths = [
'/home/container',
];
/** /**
* Returns all eggs that have this mount assigned. * Returns all eggs that have this mount assigned.
* *

View File

@ -2,6 +2,8 @@
namespace Pterodactyl\Models; namespace Pterodactyl\Models;
use Cron\CronExpression;
use Carbon\CarbonImmutable;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use Pterodactyl\Contracts\Extensions\HashidsInterface; use Pterodactyl\Contracts\Extensions\HashidsInterface;
@ -114,6 +116,20 @@ class Schedule extends Model
'next_run_at' => 'nullable|date', 'next_run_at' => 'nullable|date',
]; ];
/**
* Returns the schedule's execution crontab entry as a string.
*
* @return \Carbon\CarbonImmutable
*/
public function getNextRunDate()
{
$formatted = sprintf('%s %s %s * %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_day_of_week);
return CarbonImmutable::createFromTimestamp(
CronExpression::factory($formatted)->getNextRunDate()->getTimestamp()
);
}
/** /**
* Return a hashid encoded string to represent the ID of the schedule. * Return a hashid encoded string to represent the ID of the schedule.
* *

View File

@ -83,6 +83,13 @@ class Server extends Model
'oom_disabled' => true, 'oom_disabled' => true,
]; ];
/**
* The default relationships to load for all server models.
*
* @var string[]
*/
protected $with = ['allocation'];
/** /**
* The attributes that should be mutated to dates. * The attributes that should be mutated to dates.
* *
@ -122,7 +129,7 @@ class Server extends Model
'installed' => 'in:0,1,2', 'installed' => 'in:0,1,2',
'database_limit' => 'present|nullable|integer|min:0', 'database_limit' => 'present|nullable|integer|min:0',
'allocation_limit' => 'sometimes|nullable|integer|min:0', 'allocation_limit' => 'sometimes|nullable|integer|min:0',
'backup_limit' => 'present|integer|min:0', 'backup_limit' => 'present|nullable|integer|min:0',
]; ];
/** /**

View File

@ -31,23 +31,6 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor
return $this->getBuilder()->withCount('tasks')->where('server_id', '=', $server)->get($this->getColumns()); return $this->getBuilder()->withCount('tasks')->where('server_id', '=', $server)->get($this->getColumns());
} }
/**
* Load the tasks relationship onto the Schedule module if they are not
* already present.
*
* @param \Pterodactyl\Models\Schedule $schedule
* @param bool $refresh
* @return \Pterodactyl\Models\Schedule
*/
public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule
{
if (! $schedule->relationLoaded('tasks') || $refresh) {
$schedule->load('tasks');
}
return $schedule;
}
/** /**
* Return a schedule model with all of the associated tasks as a relationship. * Return a schedule model with all of the associated tasks as a relationship.
* *
@ -64,19 +47,4 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor
throw new RecordNotFoundException; throw new RecordNotFoundException;
} }
} }
/**
* Return all of the schedules that should be processed.
*
* @param string $timestamp
* @return \Illuminate\Support\Collection
*/
public function getSchedulesToProcess(string $timestamp): Collection
{
return $this->getBuilder()->with('tasks')
->where('is_active', true)
->where('is_processing', false)
->where('next_run_at', '<=', $timestamp)
->get($this->getColumns());
}
} }

View File

@ -270,22 +270,4 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
->where('node_id', '=', $node) ->where('node_id', '=', $node)
->paginate($limit); ->paginate($limit);
} }
/**
* Returns every server that exists for a given node.
*
* This is different from {@see loadAllServersForNode} because
* it does not paginate the response.
*
* @param int $node
*
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public function loadEveryServerForNode(int $node)
{
return $this->getBuilder()
->with('nest')
->where('node_id', '=', $node)
->get();
}
} }

View File

@ -4,7 +4,6 @@ namespace Pterodactyl\Services\Deployment;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
use Illuminate\Support\LazyCollection;
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException; use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
class FindViableNodesService class FindViableNodesService
@ -32,7 +31,7 @@ class FindViableNodesService
*/ */
public function setLocations(array $locations): self public function setLocations(array $locations): self
{ {
Assert::allInteger($locations, 'An array of location IDs should be provided when calling setLocations.'); Assert::allIntegerish($locations, 'An array of location IDs should be provided when calling setLocations.');
$this->locations = $locations; $this->locations = $locations;
@ -97,8 +96,8 @@ class FindViableNodesService
} }
$results = $query->groupBy('nodes.id') $results = $query->groupBy('nodes.id')
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [ $this->memory ]) ->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [$this->memory])
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [ $this->disk ]) ->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk])
->get() ->get()
->toBase(); ->toBase();

View File

@ -2,12 +2,12 @@
namespace Pterodactyl\Services\Schedules; namespace Pterodactyl\Services\Schedules;
use Cron\CronExpression; use Exception;
use Pterodactyl\Models\Schedule; use Pterodactyl\Models\Schedule;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Pterodactyl\Jobs\Schedule\RunTaskJob; use Pterodactyl\Jobs\Schedule\RunTaskJob;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; use Pterodactyl\Exceptions\DisplayException;
class ProcessScheduleService class ProcessScheduleService
{ {
@ -17,63 +17,66 @@ class ProcessScheduleService
private $dispatcher; private $dispatcher;
/** /**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface * @var \Illuminate\Database\ConnectionInterface
*/ */
private $scheduleRepository; private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface
*/
private $taskRepository;
/** /**
* ProcessScheduleService constructor. * ProcessScheduleService constructor.
* *
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Bus\Dispatcher $dispatcher * @param \Illuminate\Contracts\Bus\Dispatcher $dispatcher
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $scheduleRepository
* @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $taskRepository
*/ */
public function __construct( public function __construct(ConnectionInterface $connection, Dispatcher $dispatcher)
Dispatcher $dispatcher, {
ScheduleRepositoryInterface $scheduleRepository,
TaskRepositoryInterface $taskRepository
) {
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
$this->scheduleRepository = $scheduleRepository; $this->connection = $connection;
$this->taskRepository = $taskRepository;
} }
/** /**
* Process a schedule and push the first task onto the queue worker. * Process a schedule and push the first task onto the queue worker.
* *
* @param \Pterodactyl\Models\Schedule $schedule * @param \Pterodactyl\Models\Schedule $schedule
* @param bool $now
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Throwable
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function handle(Schedule $schedule) public function handle(Schedule $schedule, bool $now = false)
{ {
$this->scheduleRepository->loadTasks($schedule);
/** @var \Pterodactyl\Models\Task $task */ /** @var \Pterodactyl\Models\Task $task */
$task = $schedule->getRelation('tasks')->where('sequence_id', 1)->first(); $task = $schedule->tasks()->orderBy('sequence_id', 'asc')->first();
$formattedCron = sprintf('%s %s %s * %s', if (is_null($task)) {
$schedule->cron_minute, throw new DisplayException(
$schedule->cron_hour, 'Cannot process schedule for task execution: no tasks are registered.'
$schedule->cron_day_of_month,
$schedule->cron_day_of_week
);
$this->scheduleRepository->update($schedule->id, [
'is_processing' => true,
'next_run_at' => CronExpression::factory($formattedCron)->getNextRunDate(),
]);
$this->taskRepository->update($task->id, ['is_queued' => true]);
$this->dispatcher->dispatch(
(new RunTaskJob($task))->delay($task->time_offset)
); );
} }
$this->connection->transaction(function () use ($schedule, $task) {
$schedule->forceFill([
'is_processing' => true,
'next_run_at' => $schedule->getNextRunDate(),
])->saveOrFail();
$task->update(['is_queued' => true]);
});
$job = new RunTaskJob($task);
if (! $now) {
$this->dispatcher->dispatch($job->delay($task->time_offset));
} else {
// When using dispatchNow the RunTaskJob::failed() function is not called automatically
// so we need to manually trigger it and then continue with the exception throw.
//
// @see https://github.com/pterodactyl/panel/issues/2550
try {
$this->dispatcher->dispatchNow($job);
} catch (Exception $exception) {
$job->failed($exception);
throw $exception;
}
}
}
} }

View File

@ -4,22 +4,14 @@ namespace Pterodactyl\Services\Servers;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use GuzzleHttp\Exception\RequestException; use Pterodactyl\Models\Allocation;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class BuildModificationService class BuildModificationService
{ {
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $allocationRepository;
/** /**
* @var \Illuminate\Database\ConnectionInterface * @var \Illuminate\Database\ConnectionInterface
*/ */
@ -30,11 +22,6 @@ class BuildModificationService
*/ */
private $daemonServerRepository; private $daemonServerRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/** /**
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/ */
@ -43,23 +30,17 @@ class BuildModificationService
/** /**
* BuildModificationService constructor. * BuildModificationService constructor.
* *
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/ */
public function __construct( public function __construct(
AllocationRepositoryInterface $allocationRepository,
ServerConfigurationStructureService $structureService, ServerConfigurationStructureService $structureService,
ConnectionInterface $connection, ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository, DaemonServerRepository $daemonServerRepository
ServerRepositoryInterface $repository
) { ) {
$this->allocationRepository = $allocationRepository;
$this->daemonServerRepository = $daemonServerRepository; $this->daemonServerRepository = $daemonServerRepository;
$this->connection = $connection; $this->connection = $connection;
$this->repository = $repository;
$this->structureService = $structureService; $this->structureService = $structureService;
} }
@ -70,9 +51,8 @@ class BuildModificationService
* @param array $data * @param array $data
* @return \Pterodactyl\Models\Server * @return \Pterodactyl\Models\Server
* *
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/ */
public function handle(Server $server, array $data) public function handle(Server $server, array $data)
{ {
@ -82,48 +62,35 @@ class BuildModificationService
if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) { if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) {
try { try {
$this->allocationRepository->findFirstWhere([ Allocation::query()->where('id', $data['allocation_id'])->where('server_id', $server->id)->firstOrFail();
['id', '=', $data['allocation_id']], } catch (ModelNotFoundException $ex) {
['server_id', '=', $server->id], throw new DisplayException('The requested default allocation is not currently assigned to this server.');
]);
} catch (RecordNotFoundException $ex) {
throw new DisplayException(trans('admin/server.exceptions.default_allocation_not_found'));
} }
} }
/* @var \Pterodactyl\Models\Server $server */ // If any of these values are passed through in the data array go ahead and set
$server = $this->repository->withFreshModel()->update($server->id, [ // them correctly on the server model.
'oom_disabled' => array_get($data, 'oom_disabled'), $merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
'memory' => array_get($data, 'memory'),
'swap' => array_get($data, 'swap'), $server->forceFill(array_merge($merge, [
'io' => array_get($data, 'io'), 'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
'cpu' => array_get($data, 'cpu'), 'allocation_limit' => Arr::get($data, 'allocation_limit', 0) ?? null,
'threads' => array_get($data, 'threads'), 'backup_limit' => Arr::get($data, 'backup_limit', 0) ?? 0,
'disk' => array_get($data, 'disk'), ]))->saveOrFail();
'allocation_id' => array_get($data, 'allocation_id'),
'database_limit' => array_get($data, 'database_limit', 0), $server = $server->fresh();
'allocation_limit' => array_get($data, 'allocation_limit', 0),
'backup_limit' => array_get($data, 'backup_limit', 0),
]);
$updateData = $this->structureService->handle($server); $updateData = $this->structureService->handle($server);
try { $this->daemonServerRepository->setServer($server)->update($updateData['build'] ?? []);
$this->daemonServerRepository
->setServer($server)
->update(Arr::only($updateData, ['build']));
$this->connection->commit(); $this->connection->commit();
} catch (RequestException $exception) {
throw new DaemonConnectionException($exception);
}
return $server; return $server;
} }
/** /**
* Process the allocations being assigned in the data and ensure they * Process the allocations being assigned in the data and ensure they are available for a server.
* are available for a server.
* *
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param array $data * @param array $data
@ -132,55 +99,53 @@ class BuildModificationService
*/ */
private function processAllocations(Server $server, array &$data) private function processAllocations(Server $server, array &$data)
{ {
$firstAllocationId = null; if (empty($data['add_allocations']) && empty($data['remove_allocations'])) {
if (! array_key_exists('add_allocations', $data) && ! array_key_exists('remove_allocations', $data)) {
return; return;
} }
// Handle the addition of allocations to this server. // Handle the addition of allocations to this server. Only assign allocations that are not currently
if (array_key_exists('add_allocations', $data) && ! empty($data['add_allocations'])) { // assigned to a different server, and only allocations on the same node as the server.
$unassigned = $this->allocationRepository->getUnassignedAllocationIds($server->node_id); if (! empty($data['add_allocations'])) {
$query = Allocation::query()
->where('node_id', $server->node_id)
->whereIn('id', $data['add_allocations'])
->whereNull('server_id');
$updateIds = []; // Keep track of all the allocations we're just now adding so that we can use the first
foreach ($data['add_allocations'] as $allocation) { // one to reset the default allocation to.
if (! in_array($allocation, $unassigned)) { $freshlyAllocated = $query->pluck('id')->first();
continue;
$query->update(['server_id' => $server->id, 'notes' => null]);
} }
$firstAllocationId = $firstAllocationId ?? $allocation; if (! empty($data['remove_allocations'])) {
$updateIds[] = $allocation;
}
if (! empty($updateIds)) {
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $server->id]);
}
}
// Handle removal of allocations from this server.
if (array_key_exists('remove_allocations', $data) && ! empty($data['remove_allocations'])) {
$assigned = $server->allocations->pluck('id')->toArray();
$updateIds = [];
foreach ($data['remove_allocations'] as $allocation) { foreach ($data['remove_allocations'] as $allocation) {
if (! in_array($allocation, $assigned)) { // If we are attempting to remove the default allocation for the server, see if we can reassign
continue; // to the first provided value in add_allocations. If there is no new first allocation then we
// will throw an exception back.
if ($allocation === ($data['allocation_id'] ?? $server->allocation_id)) {
if (empty($freshlyAllocated)) {
throw new DisplayException(
'You are attempting to delete the default allocation for this server but there is no fallback allocation to use.'
);
} }
if ($allocation == $data['allocation_id']) { // Update the default allocation to be the first allocation that we are creating.
if (is_null($firstAllocationId)) { $data['allocation_id'] = $freshlyAllocated;
throw new DisplayException(trans('admin/server.exceptions.no_new_default_allocation')); }
} }
$data['allocation_id'] = $firstAllocationId; // Remove any of the allocations we got that are currently assigned to this server on
} // this node. Also set the notes to null, otherwise when re-allocated to a new server those
// notes will be carried over.
$updateIds[] = $allocation; Allocation::query()->where('node_id', $server->node_id)
} ->where('server_id', $server->id)
// Only remove the allocations that we didn't also attempt to add to the server...
if (! empty($updateIds)) { ->whereIn('id', array_diff($data['remove_allocations'], $data['add_allocations'] ?? []))
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => null]); ->update([
} 'notes' => null,
'server_id' => null,
]);
} }
} }
} }

View File

@ -7,8 +7,6 @@ use Pterodactyl\Models\Server;
class ServerConfigurationStructureService class ServerConfigurationStructureService
{ {
const REQUIRED_RELATIONS = ['allocation', 'allocations', 'egg'];
/** /**
* @var \Pterodactyl\Services\Servers\EnvironmentService * @var \Pterodactyl\Services\Servers\EnvironmentService
*/ */
@ -31,13 +29,11 @@ class ServerConfigurationStructureService
* daemon, if you modify the structure eggs will break unexpectedly. * daemon, if you modify the structure eggs will break unexpectedly.
* *
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param bool $legacy * @param bool $legacy deprecated
* @return array * @return array
*/ */
public function handle(Server $server, bool $legacy = false): array public function handle(Server $server, bool $legacy = false): array
{ {
$server->loadMissing(self::REQUIRED_RELATIONS);
return $legacy ? return $legacy ?
$this->returnLegacyFormat($server) $this->returnLegacyFormat($server)
: $this->returnCurrentFormat($server); : $this->returnCurrentFormat($server);
@ -93,6 +89,7 @@ class ServerConfigurationStructureService
* *
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @return array * @return array
* @deprecated
*/ */
protected function returnLegacyFormat(Server $server) protected function returnLegacyFormat(Server $server)
{ {

View File

@ -33,6 +33,8 @@ return [
// backup for that server lives within that folder. // backup for that server lives within that folder.
'prefix' => env('AWS_BACKUPS_BUCKET') ?? '', 'prefix' => env('AWS_BACKUPS_BUCKET') ?? '',
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'use_accelerate_endpoint' => env('AWS_BACKUPS_USE_ACCELERATE', false), 'use_accelerate_endpoint' => env('AWS_BACKUPS_USE_ACCELERATE', false),
], ],
], ],

View File

@ -1,5 +1,7 @@
<?php <?php
use Pterodactyl\Helpers\Time;
return [ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -43,6 +45,7 @@ return [
'collation' => 'utf8mb4_unicode_ci', 'collation' => 'utf8mb4_unicode_ci',
'prefix' => env('DB_PREFIX', ''), 'prefix' => env('DB_PREFIX', ''),
'strict' => env('DB_STRICT_MODE', false), 'strict' => env('DB_STRICT_MODE', false),
'timezone' => env('DB_TIMEZONE', Time::getMySQLTimezoneOffset(env('APP_TIMEZONE')))
], ],
/* /*
@ -65,6 +68,7 @@ return [
'collation' => 'utf8mb4_unicode_ci', 'collation' => 'utf8mb4_unicode_ci',
'prefix' => '', 'prefix' => '',
'strict' => false, 'strict' => false,
'timezone' => env('DB_TIMEZONE', Time::getMySQLTimezoneOffset(env('APP_TIMEZONE')))
], ],
], ],

View File

@ -58,6 +58,8 @@ return [
'secret' => env('AWS_SECRET_ACCESS_KEY'), 'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'), 'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'), 'bucket' => env('AWS_BUCKET'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
], ],
], ],
]; ];

View File

@ -188,5 +188,5 @@ return [
| |
*/ */
'same_site' => null, 'same_site' => env('SESSION_SAMESITE_COOKIE', 'lax'),
]; ];

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RemoveNullableFromScheduleNameField extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::update("UPDATE schedules SET name = 'Schedule' WHERE name IS NULL OR name = ''");
Schema::table('schedules', function (Blueprint $table) {
$table->string('name')->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('schedules', function (Blueprint $table) {
$table->string('name')->nullable()->change();
});
}
}

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2020-04-12T16:00:51-07:00", "exported_at": "2020-10-19T23:22:26+00:00",
"name": "Bungeecord", "name": "Bungeecord",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.", "description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\n# Bungeecord Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add curl\n\ncd \/mnt\/server\n\nif [ -z \"${BUNGEE_VERSION}\" ] || [ \"${BUNGEE_VERSION}\" == \"latest\" ]; then\n BUNGEE_VERSION=\"lastStableBuild\"\nfi\n\ncurl -o ${SERVER_JARFILE} https:\/\/ci.md-5.net\/job\/BungeeCord\/${BUNGEE_VERSION}\/artifact\/bootstrap\/target\/BungeeCord.jar", "script": "#!\/bin\/bash\r\n# Bungeecord Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl\r\n\r\ncd \/mnt\/server\r\n\r\nif [ -z \"${BUNGEE_VERSION}\" ] || [ \"${BUNGEE_VERSION}\" == \"latest\" ]; then\r\n BUNGEE_VERSION=\"lastStableBuild\"\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} https:\/\/ci.md-5.net\/job\/BungeeCord\/${BUNGEE_VERSION}\/artifact\/bootstrap\/target\/BungeeCord.jar",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The version of Bungeecord to download and use.", "description": "The version of Bungeecord to download and use.",
"env_variable": "BUNGEE_VERSION", "env_variable": "BUNGEE_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|alpha_num|between:1,6" "rules": "required|alpha_num|between:1,6"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name of the Jarfile to use when running Bungeecord.", "description": "The name of the Jarfile to use when running Bungeecord.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "bungeecord.jar", "default_value": "bungeecord.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
} }
] ]

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2020-05-24T12:15:13-04:00", "exported_at": "2020-10-19T23:22:28+00:00",
"name": "Forge Minecraft", "name": "Forge Minecraft",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.",
@ -17,7 +17,7 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/bash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\n#Go into main direction\r\nif [ ! -d \/mnt\/server ]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ ! -z ${FORGE_VERSION} ]; then\r\n DOWNLOAD_LINK=https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/${FORGE_VERSION}\/forge-${FORGE_VERSION}\r\nelse\r\n JSON_DATA=$(curl -sSL https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/promotions_slim.json)\r\n\r\n if [ \"${MC_VERSION}\" == \"latest\" ] || [ \"${MC_VERSION}\" == \"\" ] ; then\r\n echo -e \"getting latest recommended version of forge.\"\r\n MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains(\"recommended\")) | split(\"-\")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)\r\n \tBUILD_TYPE=recommended\r\n fi\r\n\r\n if [ \"${BUILD_TYPE}\" != \"recommended\" ] && [ \"${BUILD_TYPE}\" != \"latest\" ]; then\r\n BUILD_TYPE=recommended\r\n fi\r\n\r\n echo -e \"minecraft version: ${MC_VERSION}\"\r\n echo -e \"build type: ${BUILD_TYPE}\"\r\n\r\n ## some variables for getting versions and things\r\n FILE_SITE=$(echo -e ${JSON_DATA} | jq -r '.homepage' | sed \"s\/http:\/https:\/g\")\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" --arg BUILD_TYPE \"${BUILD_TYPE}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')\r\n\r\n ## locating the forge version\r\n if [ \"${VERSION_KEY}\" == \"\" ] && [ \"${BUILD_TYPE}\" == \"recommended\" ]; then\r\n echo -e \"dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested.\"\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains(\"recommended\"))')\r\n fi\r\n\r\n ## Error if the mc version set wasn't valid.\r\n if [ \"${VERSION_KEY}\" == \"\" ] || [ \"${VERSION_KEY}\" == \"null\" ]; then\r\n \techo -e \"The install failed because there is no valid version of forge for the version on minecraft selected.\"\r\n \texit 1\r\n fi\r\n\r\n FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY \"$VERSION_KEY\" '.promos | .[$VERSION_KEY]')\r\n\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ] || [ \"${MC_VERSION}\" == \"1.8.9\" ]; then\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ]; then\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar\r\n fi\r\n else\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar\r\n fi\r\nfi\r\n\r\n\r\n#Adding .jar when not eding by SERVER_JARFILE\r\nif [[ ! $SERVER_JARFILE = *\\.jar ]]; then\r\n SERVER_JARFILE=\"$SERVER_JARFILE.jar\"\r\nfi\r\n\r\n#Downloading jars\r\necho -e \"Downloading forge version ${FORGE_VERSION}\"\r\nif [ ! -z \"${DOWNLOAD_LINK}\" ]; then \r\n if curl --output \/dev\/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then\r\n echo -e \"installer jar download link is valid.\"\r\n else\r\n echo -e \"link is invalid closing out\"\r\n exit 2\r\n fi\r\n\r\n echo -e \"no download link closing out\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\r\n#Checking if downloaded jars exist\r\nif [ ! -f .\/installer.jar ]; then\r\n echo \"!!! Error by downloading forge version ${FORGE_VERSION} !!!\"\r\n exit\r\nfi\r\n\r\n#Installing server\r\necho -e \"Installing forge server.\\n\"\r\njava -jar installer.jar --installServer || { echo -e \"install failed\"; exit 4; }\r\n\r\nmv $FORGE_JAR $SERVER_JARFILE\r\n\r\n#Deleting installer.jar\r\necho -e \"Deleting installer.jar file.\\n\"\r\nrm -rf installer.jar", "script": "#!\/bin\/bash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\n#Go into main direction\r\nif [ ! -d \/mnt\/server ]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ ! -z ${FORGE_VERSION} ]; then\r\n DOWNLOAD_LINK=https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/${FORGE_VERSION}\/forge-${FORGE_VERSION}\r\nelse\r\n JSON_DATA=$(curl -sSL https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/promotions_slim.json)\r\n\r\n if [ \"${MC_VERSION}\" == \"latest\" ] || [ \"${MC_VERSION}\" == \"\" ] ; then\r\n echo -e \"getting latest recommended version of forge.\"\r\n MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains(\"recommended\")) | split(\"-\")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)\r\n \tBUILD_TYPE=recommended\r\n fi\r\n\r\n if [ \"${BUILD_TYPE}\" != \"recommended\" ] && [ \"${BUILD_TYPE}\" != \"latest\" ]; then\r\n BUILD_TYPE=recommended\r\n fi\r\n\r\n echo -e \"minecraft version: ${MC_VERSION}\"\r\n echo -e \"build type: ${BUILD_TYPE}\"\r\n\r\n ## some variables for getting versions and things\r\n FILE_SITE=$(echo -e ${JSON_DATA} | jq -r '.homepage' | sed \"s\/http:\/https:\/g\")\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" --arg BUILD_TYPE \"${BUILD_TYPE}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')\r\n\r\n ## locating the forge version\r\n if [ \"${VERSION_KEY}\" == \"\" ] && [ \"${BUILD_TYPE}\" == \"recommended\" ]; then\r\n echo -e \"dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested.\"\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains(\"recommended\"))')\r\n fi\r\n\r\n ## Error if the mc version set wasn't valid.\r\n if [ \"${VERSION_KEY}\" == \"\" ] || [ \"${VERSION_KEY}\" == \"null\" ]; then\r\n \techo -e \"The install failed because there is no valid version of forge for the version on minecraft selected.\"\r\n \texit 1\r\n fi\r\n\r\n FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY \"$VERSION_KEY\" '.promos | .[$VERSION_KEY]')\r\n\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ] || [ \"${MC_VERSION}\" == \"1.8.9\" ]; then\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ]; then\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar\r\n fi\r\n else\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar\r\n fi\r\nfi\r\n\r\n\r\n#Adding .jar when not eding by SERVER_JARFILE\r\nif [[ ! $SERVER_JARFILE = *\\.jar ]]; then\r\n SERVER_JARFILE=\"$SERVER_JARFILE.jar\"\r\nfi\r\n\r\n#Downloading jars\r\necho -e \"Downloading forge version ${FORGE_VERSION}\"\r\necho -e \"Download link is ${DOWNLOAD_LINK}\"\r\nif [ ! -z \"${DOWNLOAD_LINK}\" ]; then \r\n if curl --output \/dev\/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then\r\n echo -e \"installer jar download link is valid.\"\r\n else\r\n echo -e \"link is invalid closing out\"\r\n exit 2\r\n fi\r\nelse\r\n echo -e \"no download link closing out\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\r\n#Checking if downloaded jars exist\r\nif [ ! -f .\/installer.jar ]; then\r\n echo \"!!! Error by downloading forge version ${FORGE_VERSION} !!!\"\r\n exit\r\nfi\r\n\r\n#Installing server\r\necho -e \"Installing forge server.\\n\"\r\njava -jar installer.jar --installServer || { echo -e \"install failed\"; exit 4; }\r\n\r\nmv $FORGE_JAR $SERVER_JARFILE\r\n\r\n#Deleting installer.jar\r\necho -e \"Deleting installer.jar file.\\n\"\r\nrm -rf installer.jar",
"container": "openjdk:8-jdk-slim", "container": "openjdk:8-jdk-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
@ -28,8 +28,8 @@
"description": "The name of the Jarfile to use when running Forge Mod.", "description": "The name of the Jarfile to use when running Forge Mod.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "server.jar", "default_value": "server.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The version of minecraft you want to install for.\r\n\r\nLeaving latest will install the latest recommended version.", "description": "The version of minecraft you want to install for.\r\n\r\nLeaving latest will install the latest recommended version.",
"env_variable": "MC_VERSION", "env_variable": "MC_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:9" "rules": "required|string|max:9"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The type of server jar to download from forge.\r\n\r\nValid types are \"recommended\" and \"latest\".", "description": "The type of server jar to download from forge.\r\n\r\nValid types are \"recommended\" and \"latest\".",
"env_variable": "BUILD_TYPE", "env_variable": "BUILD_TYPE",
"default_value": "recommended", "default_value": "recommended",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:20" "rules": "required|string|max:20"
}, },
{ {
@ -55,8 +55,8 @@
"description": "Gets an exact version.\r\n\r\nEx. 1.15.2-31.2.4\r\n\r\nOverrides MC_VERSION and BUILD_TYPE. If it fails to download the server files it will fail to install.", "description": "Gets an exact version.\r\n\r\nEx. 1.15.2-31.2.4\r\n\r\nOverrides MC_VERSION and BUILD_TYPE. If it fails to download the server files it will fail to install.",
"env_variable": "FORGE_VERSION", "env_variable": "FORGE_VERSION",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:20" "rules": "required|string|max:20"
} }
] ]

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-08-01T04:49:37-04:00", "exported_at": "2020-10-25T21:03:55+00:00",
"name": "Paper", "name": "Paper",
"author": "parker@pterodactyl.io", "author": "parker@pterodactyl.io",
"description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.", "description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\r\n# Paper Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk add --no-cache --update curl jq\r\n\r\nif [ -n \"${DL_PATH}\" ]; then\r\n echo -e \"using supplied download url\"\r\n DOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\nelse\r\n VER_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | IN($VERSION)' | grep true`\r\n LATEST_PAPER_VERSION=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r '.versions' | jq -r '.[0]'`\r\n \r\n if [ \"${VER_EXISTS}\" == \"true\" ]; then\r\n echo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\r\n else\r\n echo -e \"Using the latest paper version\"\r\n MINECRAFT_VERSION=${LATEST_PAPER_VERSION}\r\n fi\r\n \r\n BUILD_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds.all[] | IN($BUILD)' | grep true`\r\n LATEST_PAPER_BUILD=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r '.builds.latest'`\r\n \r\n if [ \"${BUILD_EXISTS}\" == \"true\" ] || [ ${BUILD_NUMBER} == \"latest\" ]; then\r\n echo -e \"Build is valid. Using version ${BUILD_NUMBER}\"\r\n else\r\n echo -e \"Using the latest paper build\"\r\n BUILD_NUMBER=${LATEST_PAPER_BUILD}\r\n fi\r\n \r\n echo \"Version being downloaded\"\r\n echo -e \"MC Version: ${MINECRAFT_VERSION}\"\r\n echo -e \"Build: ${BUILD_NUMBER}\"\r\n DOWNLOAD_URL=https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION}\/${BUILD_NUMBER}\/download \r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\r\n\r\nif [ -f ${SERVER_JARFILE} ]; then\r\n mv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\r\n\r\nif [ ! -f server.properties ]; then\r\n echo -e \"Downloading MC server.properties\"\r\n curl -o server.properties https:\/\/raw.githubusercontent.com\/parkervcp\/eggs\/master\/minecraft_java\/server.properties\r\nfi", "script": "#!\/bin\/bash\r\n# Paper Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\nif [ -n \"${DL_PATH}\" ]; then\r\n echo -e \"using supplied download url\"\r\n DOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\nelse\r\n VER_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | contains($VERSION)' | grep true`\r\n LATEST_PAPER_VERSION=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r '.versions' | jq -r '.[0]'`\r\n \r\n if [ \"${VER_EXISTS}\" == \"true\" ]; then\r\n echo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\r\n else\r\n echo -e \"Using the latest paper version\"\r\n MINECRAFT_VERSION=${LATEST_PAPER_VERSION}\r\n fi\r\n \r\n BUILD_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds.all[] | IN($BUILD)' | grep true`\r\n LATEST_PAPER_BUILD=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r '.builds.latest'`\r\n \r\n if [ \"${BUILD_EXISTS}\" == \"true\" ] || [ ${BUILD_NUMBER} == \"latest\" ]; then\r\n echo -e \"Build is valid. Using version ${BUILD_NUMBER}\"\r\n else\r\n echo -e \"Using the latest paper build\"\r\n BUILD_NUMBER=${LATEST_PAPER_BUILD}\r\n fi\r\n \r\n echo \"Version being downloaded\"\r\n echo -e \"MC Version: ${MINECRAFT_VERSION}\"\r\n echo -e \"Build: ${BUILD_NUMBER}\"\r\n DOWNLOAD_URL=https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION}\/${BUILD_NUMBER}\/download \r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\r\n\r\nif [ -f ${SERVER_JARFILE} ]; then\r\n mv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\r\n\r\nif [ ! -f server.properties ]; then\r\n echo -e \"Downloading MC server.properties\"\r\n curl -o server.properties https:\/\/raw.githubusercontent.com\/parkervcp\/eggs\/master\/minecraft\/java\/server.properties\r\nfi",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The version of minecraft to download. \r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.", "description": "The version of minecraft to download. \r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.",
"env_variable": "MINECRAFT_VERSION", "env_variable": "MINECRAFT_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "nullable|string|max:20" "rules": "nullable|string|max:20"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name of the server jarfile to run the server with.", "description": "The name of the server jarfile to run the server with.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "server.jar", "default_value": "server.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:20" "rules": "required|string|max:20"
}, },
{ {
@ -46,8 +46,8 @@
"description": "A URL to use to download a server.jar rather than the ones in the install script. This is not user viewable.", "description": "A URL to use to download a server.jar rather than the ones in the install script. This is not user viewable.",
"env_variable": "DL_PATH", "env_variable": "DL_PATH",
"default_value": "", "default_value": "",
"user_viewable": 0, "user_viewable": false,
"user_editable": 0, "user_editable": false,
"rules": "nullable|string" "rules": "nullable|string"
}, },
{ {
@ -55,8 +55,8 @@
"description": "The build number for the paper release.\r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.", "description": "The build number for the paper release.\r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.",
"env_variable": "BUILD_NUMBER", "env_variable": "BUILD_NUMBER",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|string|max:20" "rules": "required|string|max:20"
} }
] ]

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2017-11-03T22:20:03-05:00", "exported_at": "2020-10-19T23:26:54+00:00",
"name": "Sponge (SpongeVanilla)", "name": "Sponge (SpongeVanilla)",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.", "description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\n# Sponge Installation Script\n#\n# Server Files: \/mnt\/server\n\napk update\napk add curl\n\ncd \/mnt\/server\n\ncurl -sSL \"https:\/\/repo.spongepowered.org\/maven\/org\/spongepowered\/spongevanilla\/${SPONGE_VERSION}\/spongevanilla-${SPONGE_VERSION}.jar\" -o ${SERVER_JARFILE}", "script": "#!\/bin\/bash\r\n# Sponge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\napt update\r\napt install -y curl\r\n\r\ncd \/mnt\/server\r\n\r\ncurl -sSL \"https:\/\/repo.spongepowered.org\/maven\/org\/spongepowered\/spongevanilla\/${SPONGE_VERSION}\/spongevanilla-${SPONGE_VERSION}.jar\" -o ${SERVER_JARFILE}",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The version of SpongeVanilla to download and use.", "description": "The version of SpongeVanilla to download and use.",
"env_variable": "SPONGE_VERSION", "env_variable": "SPONGE_VERSION",
"default_value": "1.11.2-6.1.0-BETA-21", "default_value": "1.11.2-6.1.0-BETA-21",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/" "rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name of the Jarfile to use when running SpongeVanilla.", "description": "The name of the Jarfile to use when running SpongeVanilla.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "server.jar", "default_value": "server.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
} }
] ]

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-25T19:55:01-05:00", "exported_at": "2020-10-23T23:04:17+00:00",
"name": "Vanilla Minecraft", "name": "Vanilla Minecraft",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.", "description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk add curl --no-cache --update jq\r\n\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\n\r\necho -e \"latest version is $LATEST_VERSION\"\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelse\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $VANILLA_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nfi\r\n\r\nDOWNLOAD_URL=$(curl ${MANIFEST_URL} | jq .downloads.server | jq -r '. | .url')\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} $DOWNLOAD_URL\"\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL\r\n\r\necho -e \"Install Complete\"", "script": "#!\/bin\/bash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y jq curl\r\n\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\n\r\necho -e \"latest version is $LATEST_VERSION\"\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelse\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $VANILLA_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nfi\r\n\r\nDOWNLOAD_URL=$(curl ${MANIFEST_URL} | jq .downloads.server | jq -r '. | .url')\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} $DOWNLOAD_URL\"\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL\r\n\r\necho -e \"Install Complete\"",
"container": "alpine:3.10", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The name of the server jarfile to run the server with.", "description": "The name of the server jarfile to run the server with.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "server.jar", "default_value": "server.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version.", "description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version.",
"env_variable": "VANILLA_VERSION", "env_variable": "VANILLA_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|between:3,15" "rules": "required|string|between:3,15"
} }
] ]

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-01-21T16:58:36-06:00", "exported_at": "2020-10-20T00:03:09+00:00",
"name": "Rust", "name": "Rust",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.", "description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "apt update\r\napt -y --no-install-recommends install curl unzip lib32gcc1 ca-certificates\r\ncd \/tmp\r\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\r\n\r\nmkdir -p \/mnt\/server\/steam\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steam\r\ncd \/mnt\/server\/steam\r\nchown -R root:root \/mnt\r\n\r\nexport HOME=\/mnt\/server\r\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 258550 +quit\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v \/mnt\/server\/steam\/linux32\/steamclient.so \/mnt\/server\/.steam\/sdk32\/steamclient.so", "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'debian:buster-slim'\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\nSRCDS_APPID=258550\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
"container": "ubuntu:16.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The name of your server in the public server list.", "description": "The name of your server in the public server list.",
"env_variable": "HOSTNAME", "env_variable": "HOSTNAME",
"default_value": "A Rust Server", "default_value": "A Rust Server",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:40" "rules": "required|string|max:40"
}, },
{ {
@ -37,8 +37,8 @@
"description": "Set whether you want the server to use and auto update OxideMod or not. Valid options are \"1\" for true and \"0\" for false.", "description": "Set whether you want the server to use and auto update OxideMod or not. Valid options are \"1\" for true and \"0\" for false.",
"env_variable": "OXIDE", "env_variable": "OXIDE",
"default_value": "0", "default_value": "0",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|boolean" "rules": "required|boolean"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The world file for Rust to use.", "description": "The world file for Rust to use.",
"env_variable": "LEVEL", "env_variable": "LEVEL",
"default_value": "Procedural Map", "default_value": "Procedural Map",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:20" "rules": "required|string|max:20"
}, },
{ {
@ -55,8 +55,8 @@
"description": "The description under your server title. Commonly used for rules & info. Use \\n for newlines.", "description": "The description under your server title. Commonly used for rules & info. Use \\n for newlines.",
"env_variable": "DESCRIPTION", "env_variable": "DESCRIPTION",
"default_value": "Powered by Pterodactyl", "default_value": "Powered by Pterodactyl",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string" "rules": "required|string"
}, },
{ {
@ -64,8 +64,8 @@
"description": "The URL for your server. This is what comes up when clicking the \"Visit Website\" button.", "description": "The URL for your server. This is what comes up when clicking the \"Visit Website\" button.",
"env_variable": "SERVER_URL", "env_variable": "SERVER_URL",
"default_value": "http:\/\/pterodactyl.io", "default_value": "http:\/\/pterodactyl.io",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|url" "rules": "nullable|url"
}, },
{ {
@ -73,8 +73,8 @@
"description": "The world size for a procedural map.", "description": "The world size for a procedural map.",
"env_variable": "WORLD_SIZE", "env_variable": "WORLD_SIZE",
"default_value": "3000", "default_value": "3000",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer" "rules": "required|integer"
}, },
{ {
@ -82,8 +82,8 @@
"description": "The seed for a procedural map.", "description": "The seed for a procedural map.",
"env_variable": "WORLD_SEED", "env_variable": "WORLD_SEED",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|string" "rules": "nullable|string"
}, },
{ {
@ -91,8 +91,8 @@
"description": "The maximum amount of players allowed in the server at once.", "description": "The maximum amount of players allowed in the server at once.",
"env_variable": "MAX_PLAYERS", "env_variable": "MAX_PLAYERS",
"default_value": "40", "default_value": "40",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer" "rules": "required|integer"
}, },
{ {
@ -100,8 +100,8 @@
"description": "The header image for the top of your server listing.", "description": "The header image for the top of your server listing.",
"env_variable": "SERVER_IMG", "env_variable": "SERVER_IMG",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|url" "rules": "nullable|url"
}, },
{ {
@ -109,8 +109,8 @@
"description": "Port for RCON connections.", "description": "Port for RCON connections.",
"env_variable": "RCON_PORT", "env_variable": "RCON_PORT",
"default_value": "28016", "default_value": "28016",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|integer" "rules": "required|integer"
}, },
{ {
@ -118,17 +118,17 @@
"description": "RCON access password.", "description": "RCON access password.",
"env_variable": "RCON_PASS", "env_variable": "RCON_PASS",
"default_value": "CHANGEME", "default_value": "CHANGEME",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:64" "rules": "required|string|max:64"
}, },
{ {
"name": "Save Interval", "name": "Save Interval",
"description": "Sets the servers auto-save interval in seconds.", "description": "Sets the server\u2019s auto-save interval in seconds.",
"env_variable": "SAVEINTERVAL", "env_variable": "SAVEINTERVAL",
"default_value": "60", "default_value": "60",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer" "rules": "required|integer"
}, },
{ {
@ -136,8 +136,8 @@
"description": "Add additional startup parameters to the server.", "description": "Add additional startup parameters to the server.",
"env_variable": "ADDITIONAL_ARGS", "env_variable": "ADDITIONAL_ARGS",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|string" "rules": "nullable|string"
} }
] ]

View File

@ -3,78 +3,87 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-10-29T20:51:32+01:00", "exported_at": "2020-10-19T23:29:06+00:00",
"name": "Ark: Survival Evolved", "name": "Ark: Survival Evolved",
"author": "support@pterodactyl.io", "author": "dev@shepper.fr",
"description": "As a man or woman stranded, naked, freezing, and starving on the unforgiving shores of a mysterious island called ARK, use your skill and cunning to kill or tame and ride the plethora of leviathan dinosaurs and other primeval creatures roaming the land. Hunt, harvest resources, craft items, grow crops, research technologies, and build shelters to withstand the elements and store valuables, all while teaming up with (or preying upon) hundreds of other players to survive, dominate... and escape! \u2014 Gamepedia: ARK", "description": "As a man or woman stranded, naked, freezing, and starving on the unforgiving shores of a mysterious island called ARK, use your skill and cunning to kill or tame and ride the plethora of leviathan dinosaurs and other primeval creatures roaming the land. Hunt, harvest resources, craft items, grow crops, research technologies, and build shelters to withstand the elements and store valuables, all while teaming up with (or preying upon) hundreds of other players to survive, dominate... and escape! \u2014 Gamepedia: ARK",
"image": "quay.io\/pterodactyl\/core:source", "image": "quay.io\/parkervcp\/pterodactyl-images:debian_source",
"startup": "\"cd ShooterGame\/Binaries\/Linux && .\/ShooterGameServer {{SERVER_MAP}}?listen?SessionName='{{SESSION_NAME}}'?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{PORT}}?MaxPlayers={{SERVER_MAX_PLAYERS}}?RCONPort={{RCON_PORT}}?QueryPort={{QUERY_PORT}}?RCONEnabled={{ENABLE_RCON}} -server -log\"", "startup": "rmv() { echo -e \"stoppping server\"; rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD} -c saveworld && rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD} -c DoExit; }; trap rmv 15; cd ShooterGame\/Binaries\/Linux && .\/ShooterGameServer {{SERVER_MAP}}?listen?SessionName=\"{{SESSION_NAME}}\"?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{SERVER_PORT}}?RCONPort={{RCON_PORT}}?QueryPort={{QUERY_PORT}}?RCONEnabled={{ENABLE_RCON}}$( [ \"$BATTLE_EYE\" == \"0\" ] || printf %s '?-NoBattlEye' ) -server -log & until echo \"waiting for rcon connection...\"; rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD}; do sleep 5; done",
"config": { "config": {
"files": "{}", "files": "{}",
"startup": "{\r\n \"done\": \"Setting breakpad minidump AppID = 346110\",\r\n \"userInteraction\": []\r\n}", "startup": "{\r\n \"done\": \"Waiting commands for 127.0.0.1:\",\r\n \"userInteraction\": []\r\n}",
"logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}", "logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}",
"stop": "^C" "stop": "^C"
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/bash\r\n# ARK: Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\ncd \/tmp\r\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\r\n\r\nmkdir -p \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\n\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\n\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\n\r\nexport HOME=\/mnt\/server\r\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 376030 +quit\r\n\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\ncd \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\n\r\nln -sf ..\/..\/..\/..\/..\/Steam\/steamapps steamapps\r\n\r\ncd \/mnt\/server", "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'ubuntu:18.04'\r\napt -y update\r\napt -y --no-install-recommends --no-install-suggests install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\n\r\nmkdir -p \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\n\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so\r\n\r\n## create a symbolic link for loading mods\r\ncd \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\nln -sf ..\/..\/..\/..\/..\/Steam\/steamapps steamapps\r\ncd \/mnt\/server",
"container": "ubuntu:16.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
{
"name": "Server Name",
"description": "ARK server name",
"env_variable": "SESSION_NAME",
"default_value": "ARK SERVER",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|string|max:128"
},
{ {
"name": "Server Password", "name": "Server Password",
"description": "If specified, players must provide this password to join the server.", "description": "If specified, players must provide this password to join the server.",
"env_variable": "ARK_PASSWORD", "env_variable": "ARK_PASSWORD",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|alpha_dash|between:1,100" "rules": "nullable|alpha_dash|between:1,100"
}, },
{ {
"name": "Admin Password", "name": "Admin Password",
"description": "If specified, players must provide this password (via the in-game console) to gain access to administrator commands on the server.", "description": "If specified, players must provide this password (via the in-game console) to gain access to administrator commands on the server.",
"env_variable": "ARK_ADMIN_PASSWORD", "env_variable": "ARK_ADMIN_PASSWORD",
"default_value": "", "default_value": "PleaseChangeMe",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|alpha_dash|between:1,100" "rules": "nullable|alpha_dash|between:1,100"
}, },
{ {
"name": "Server Port", "name": "Server Map",
"description": "ARK server port used by client.", "description": "Available Maps: TheIsland, TheCenter, Ragnarok, ScorchedEarth_P, Aberration_P, Extinction, Valguero_P, Genesis",
"env_variable": "PORT", "env_variable": "SERVER_MAP",
"default_value": "7777", "default_value": "TheIsland",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|numeric" "rules": "required|string|max:20"
},
{
"name": "App ID",
"description": "ARK steam app id for auto updates. Leave blank to avoid auto update.",
"env_variable": "SRCDS_APPID",
"default_value": "376030",
"user_viewable": true,
"user_editable": false,
"rules": "nullable|numeric"
},
{
"name": "Server Name",
"description": "ARK server name",
"env_variable": "SESSION_NAME",
"default_value": "A Pterodactyl Hosted ARK Server",
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:128"
}, },
{ {
"name": "Use Rcon", "name": "Use Rcon",
"description": "Enable or disable rcon system. (true or false)", "description": "Enable or disable rcon system. (true or false)\r\n\r\nDefault True for the console to work.",
"env_variable": "ENABLE_RCON", "env_variable": "ENABLE_RCON",
"default_value": "false", "default_value": "True",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": false,
"rules": "required|string|max:5" "rules": "required|string|in:True,False"
}, },
{ {
"name": "Rcon Port", "name": "Rcon Port",
"description": "ARK rcon port used by rcon tools.", "description": "ARK rcon port used by rcon tools.",
"env_variable": "RCON_PORT", "env_variable": "RCON_PORT",
"default_value": "27020", "default_value": "27020",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|numeric" "rules": "required|numeric"
}, },
{ {
@ -82,36 +91,27 @@
"description": "ARK query port used by steam server browser and ark client server browser.", "description": "ARK query port used by steam server browser and ark client server browser.",
"env_variable": "QUERY_PORT", "env_variable": "QUERY_PORT",
"default_value": "27015", "default_value": "27015",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|numeric" "rules": "required|numeric"
}, },
{ {
"name": "Maximum Players", "name": "Auto-update server",
"description": "Specifies the maximum number of players that can play on the server simultaneously.", "description": "This is to enable auto-updating for servers.\r\n\r\nDefault is 0. Set to 1 to update",
"env_variable": "SERVER_MAX_PLAYERS", "env_variable": "AUTO_UPDATE",
"default_value": "20", "default_value": "0",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|numeric|digits_between:1,4" "rules": "required|boolean"
}, },
{ {
"name": "App ID", "name": "Ballte Eye",
"description": "ARK steam app id for auto updates. Leave blank to avoid auto update.", "description": "Enable BattleEye\r\n\r\n0 to disable\r\n1 to enable\r\n\r\ndefault=\"1\"",
"env_variable": "SRCDS_APPID", "env_variable": "BATTLE_EYE",
"default_value": "376030", "default_value": "1",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": true,
"rules": "nullable|numeric" "rules": "required|boolean"
},
{
"name": "Server Map",
"description": "Available Maps: TheIsland, TheCenter, Ragnarok, ScorchedEarth_P, Aberration_P, Extinction",
"env_variable": "SERVER_MAP",
"default_value": "TheIsland",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|string|max:20"
} }
] ]
} }

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:52:19-05:00", "exported_at": "2020-10-19T23:29:57+00:00",
"name": "Counter-Strike: Global Offensive", "name": "Counter-Strike: Global Offensive",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.", "description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.",
@ -28,8 +28,8 @@
"description": "The default map for the server.", "description": "The default map for the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "de_dust2", "default_value": "de_dust2",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|alpha_dash" "rules": "required|string|alpha_dash"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The Steam Account Token required for the server to be displayed publicly.", "description": "The Steam Account Token required for the server to be displayed publicly.",
"env_variable": "STEAM_ACC", "env_variable": "STEAM_ACC",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|alpha_num|size:32" "rules": "required|string|alpha_num|size:32"
}, },
{ {
@ -46,8 +46,8 @@
"description": "Required for game to update on server restart. Do not modify this.", "description": "Required for game to update on server restart. Do not modify this.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "740", "default_value": "740",
"user_viewable": 0, "user_viewable": false,
"user_editable": 0, "user_editable": false,
"rules": "required|string|max:20" "rules": "required|string|max:20"
} }
] ]

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:54:26-05:00", "exported_at": "2020-10-19T23:33:52+00:00",
"name": "Custom Source Engine Game", "name": "Custom Source Engine Game",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "This option allows modifying the startup arguments and other details to run a custom SRCDS based game on the panel.", "description": "This option allows modifying the startup arguments and other details to run a custom SRCDS based game on the panel.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'ubuntu:18.04'\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so", "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'debian:buster-slim'\r\n\r\n##\r\n#\r\n# Variables\r\n# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.\r\n# WINDOWS_INSTALL - if it's a windows server you want to install set to 1\r\n# SRCDS_APPID - steam app id ffound here - https:\/\/developer.valvesoftware.com\/wiki\/Dedicated_Servers_List\r\n# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.\r\n#\r\n##\r\n\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
"container": "ubuntu:18.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The ID corresponding to the game to download and run using SRCDS.", "description": "The ID corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|numeric|digits_between:1,6" "rules": "required|numeric|digits_between:1,6"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name corresponding to the game to download and run using SRCDS.", "description": "The name corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_GAME", "env_variable": "SRCDS_GAME",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|alpha_dash|between:1,100" "rules": "required|alpha_dash|between:1,100"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The default map for the server.", "description": "The default map for the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|alpha_dash" "rules": "required|string|alpha_dash"
} }
] ]

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:56:42-05:00", "exported_at": "2020-10-19T23:34:44+00:00",
"name": "Garrys Mod", "name": "Garrys Mod",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.", "description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'ubuntu:18.04'\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so\r\n\r\n# Creating needed default files for the game\r\ncd \/mnt\/server\/garrysmod\/lua\/autorun\/server\r\necho '\r\n-- Docs: https:\/\/wiki.garrysmod.com\/page\/resource\/AddWorkshop\r\n-- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID\r\n-- Use https:\/\/beta.configcreator.com\/create\/gmod\/resources.lua to easily create a list based on your collection ID\r\n\r\nresource.AddWorkshop( \"\" )\r\n' > workshop.lua\r\n\r\ncd \/mnt\/server\/garrysmod\/cfg\r\necho '\r\n\/\/ Please do not set RCon in here, use the startup parameters.\r\n\r\nhostname\t\t\"New Gmod Server\"\r\nsv_password\t\t\"\"\r\nsv_loadingurl \"\"\r\n\r\n\/\/ Steam Server List Settings\r\nsv_region \"255\"\r\nsv_lan \"0\"\r\nsv_max_queries_sec_global \"30000\"\r\nsv_max_queries_window \"45\"\r\nsv_max_queries_sec \"5\"\r\n\r\n\/\/ Server Limits\r\nsbox_maxprops\t\t100\r\nsbox_maxragdolls\t5\r\nsbox_maxnpcs\t\t10\r\nsbox_maxballoons\t10\r\nsbox_maxeffects\t\t10\r\nsbox_maxdynamite\t10\r\nsbox_maxlamps\t\t10\r\nsbox_maxthrusters\t10\r\nsbox_maxwheels\t\t10\r\nsbox_maxhoverballs\t10\r\nsbox_maxvehicles\t20\r\nsbox_maxbuttons\t\t10\r\nsbox_maxsents\t\t20\r\nsbox_maxemitters\t5\r\nsbox_godmode\t\t0\r\nsbox_noclip\t\t 0\r\n\r\n\/\/ Network Settings - Please keep these set to default.\r\n\r\nsv_minrate\t\t75000\r\nsv_maxrate\t\t0\r\ngmod_physiterations\t2\r\nnet_splitpacket_maxrate\t45000\r\ndecalfrequency\t\t12 \r\n\r\n\/\/ Execute Ban Files - Please do not edit\r\nexec banned_ip.cfg \r\nexec banned_user.cfg \r\n\r\n\/\/ Add custom lines under here\r\n' > server.cfg", "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'debian:buster-slim'\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so\r\n\r\n# Creating needed default files for the game\r\ncd \/mnt\/server\/garrysmod\/lua\/autorun\/server\r\necho '\r\n-- Docs: https:\/\/wiki.garrysmod.com\/page\/resource\/AddWorkshop\r\n-- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID\r\n-- Use https:\/\/beta.configcreator.com\/create\/gmod\/resources.lua to easily create a list based on your collection ID\r\n\r\nresource.AddWorkshop( \"\" )\r\n' > workshop.lua\r\n\r\ncd \/mnt\/server\/garrysmod\/cfg\r\necho '\r\n\/\/ Please do not set RCon in here, use the startup parameters.\r\n\r\nhostname\t\t\"New Gmod Server\"\r\nsv_password\t\t\"\"\r\nsv_loadingurl \"\"\r\n\r\n\/\/ Steam Server List Settings\r\nsv_region \"255\"\r\nsv_lan \"0\"\r\nsv_max_queries_sec_global \"30000\"\r\nsv_max_queries_window \"45\"\r\nsv_max_queries_sec \"5\"\r\n\r\n\/\/ Server Limits\r\nsbox_maxprops\t\t100\r\nsbox_maxragdolls\t5\r\nsbox_maxnpcs\t\t10\r\nsbox_maxballoons\t10\r\nsbox_maxeffects\t\t10\r\nsbox_maxdynamite\t10\r\nsbox_maxlamps\t\t10\r\nsbox_maxthrusters\t10\r\nsbox_maxwheels\t\t10\r\nsbox_maxhoverballs\t10\r\nsbox_maxvehicles\t20\r\nsbox_maxbuttons\t\t10\r\nsbox_maxsents\t\t20\r\nsbox_maxemitters\t5\r\nsbox_godmode\t\t0\r\nsbox_noclip\t\t 0\r\n\r\n\/\/ Network Settings - Please keep these set to default.\r\n\r\nsv_minrate\t\t75000\r\nsv_maxrate\t\t0\r\ngmod_physiterations\t2\r\nnet_splitpacket_maxrate\t45000\r\ndecalfrequency\t\t12 \r\n\r\n\/\/ Execute Ban Files - Please do not edit\r\nexec banned_ip.cfg \r\nexec banned_user.cfg \r\n\r\n\/\/ Add custom lines under here\r\n' > server.cfg",
"container": "ubuntu:18.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The default map for the server.", "description": "The default map for the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "gm_flatgrass", "default_value": "gm_flatgrass",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|alpha_dash" "rules": "required|string|alpha_dash"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The Steam Account Token required for the server to be displayed publicly.", "description": "The Steam Account Token required for the server to be displayed publicly.",
"env_variable": "STEAM_ACC", "env_variable": "STEAM_ACC",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|string|alpha_num|size:32" "rules": "nullable|string|alpha_num|size:32"
}, },
{ {
@ -46,8 +46,8 @@
"description": "Required for game to update on server restart. Do not modify this.", "description": "Required for game to update on server restart. Do not modify this.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "4020", "default_value": "4020",
"user_viewable": 0, "user_viewable": false,
"user_editable": 0, "user_editable": false,
"rules": "required|string|max:20" "rules": "required|string|max:20"
}, },
{ {
@ -55,8 +55,8 @@
"description": "The ID of your workshop collection (the numbers at the end of the URL)", "description": "The ID of your workshop collection (the numbers at the end of the URL)",
"env_variable": "WORKSHOP_ID", "env_variable": "WORKSHOP_ID",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|integer" "rules": "nullable|integer"
}, },
{ {
@ -64,8 +64,8 @@
"description": "The gamemode of your server.", "description": "The gamemode of your server.",
"env_variable": "GAMEMODE", "env_variable": "GAMEMODE",
"default_value": "sandbox", "default_value": "sandbox",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string" "rules": "required|string"
}, },
{ {
@ -73,8 +73,8 @@
"description": "The maximum amount of players allowed on your game server.", "description": "The maximum amount of players allowed on your game server.",
"env_variable": "MAX_PLAYERS", "env_variable": "MAX_PLAYERS",
"default_value": "32", "default_value": "32",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer|max:128" "rules": "required|integer|max:128"
}, },
{ {
@ -82,8 +82,8 @@
"description": "The tickrate defines how fast the server will update each entities location.", "description": "The tickrate defines how fast the server will update each entities location.",
"env_variable": "TICKRATE", "env_variable": "TICKRATE",
"default_value": "22", "default_value": "22",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer|max:100" "rules": "required|integer|max:100"
} }
] ]

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:57:32-05:00", "exported_at": "2020-10-19T23:35:42+00:00",
"name": "Insurgency", "name": "Insurgency",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Take to the streets for intense close quarters combat, where a team's survival depends upon securing crucial strongholds and destroying enemy supply in this multiplayer and cooperative Source Engine based experience.", "description": "Take to the streets for intense close quarters combat, where a team's survival depends upon securing crucial strongholds and destroying enemy supply in this multiplayer and cooperative Source Engine based experience.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'ubuntu:18.04'\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so", "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'debian:buster-slim'\r\n\r\n##\r\n#\r\n# Variables\r\n# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.\r\n# WINDOWS_INSTALL - if it's a windows server you want to install set to 1\r\n# SRCDS_APPID - steam app id ffound here - https:\/\/developer.valvesoftware.com\/wiki\/Dedicated_Servers_List\r\n# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.\r\n#\r\n##\r\n\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
"container": "ubuntu:18.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The ID corresponding to the game to download and run using SRCDS.", "description": "The ID corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "237410", "default_value": "237410",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^(237410)$\/" "rules": "required|regex:\/^(237410)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name corresponding to the game to download and run using SRCDS.", "description": "The name corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_GAME", "env_variable": "SRCDS_GAME",
"default_value": "insurgency", "default_value": "insurgency",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^(insurgency)$\/" "rules": "required|regex:\/^(insurgency)$\/"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The default map to use when starting the server.", "description": "The default map to use when starting the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "sinjar", "default_value": "sinjar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^(\\w{1,20})$\/" "rules": "required|regex:\/^(\\w{1,20})$\/"
} }
] ]

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:58:48-05:00", "exported_at": "2020-10-19T23:36:44+00:00",
"name": "Team Fortress 2", "name": "Team Fortress 2",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake.", "description": "Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'ubuntu:18.04'\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so", "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'debian:buster-slim'\r\n\r\n##\r\n#\r\n# Variables\r\n# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.\r\n# WINDOWS_INSTALL - if it's a windows server you want to install set to 1\r\n# SRCDS_APPID - steam app id ffound here - https:\/\/developer.valvesoftware.com\/wiki\/Dedicated_Servers_List\r\n# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.\r\n#\r\n##\r\n\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
"container": "ubuntu:18.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The ID corresponding to the game to download and run using SRCDS.", "description": "The ID corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "232250", "default_value": "232250",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^(232250)$\/" "rules": "required|regex:\/^(232250)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name corresponding to the game to download and run using SRCDS.", "description": "The name corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_GAME", "env_variable": "SRCDS_GAME",
"default_value": "tf", "default_value": "tf",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^(tf)$\/" "rules": "required|regex:\/^(tf)$\/"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The default map to use when starting the server.", "description": "The default map to use when starting the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "cp_dustbowl", "default_value": "cp_dustbowl",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^(\\w{1,20})$\/" "rules": "required|regex:\/^(\\w{1,20})$\/"
} }
] ]

View File

@ -1,45 +0,0 @@
{
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
"meta": {
"version": "PTDL_v1"
},
"exported_at": "2018-01-21T17:01:31-06:00",
"name": "Terraria Server (TShock)",
"author": "support@pterodactyl.io",
"description": "TShock is a server modification for Terraria, written in C#, and based upon the Terraria Server API. It uses JSON for configuration management, and offers several features not present in the Terraria Server normally.",
"image": "quay.io\/pterodactyl\/core:mono",
"startup": null,
"config": {
"files": "{\"tshock\/config.json\":{\"parser\": \"json\", \"find\":{\"ServerPort\": \"{{server.build.default.port}}\", \"MaxSlots\": \"{{server.build.env.MAX_SLOTS}}\"}}}",
"startup": "{\"done\": \"Type 'help' for a list of commands\", \"userInteraction\": []}",
"logs": "{\"custom\": false, \"location\": \"ServerLog.txt\"}",
"stop": "exit"
},
"scripts": {
"installation": {
"script": "#!\/bin\/ash\n# TShock Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add curl unzip\n\ncd \/tmp\n\ncurl -sSLO https:\/\/github.com\/NyxStudios\/TShock\/releases\/download\/v${T_VERSION}\/tshock_${T_VERSION}.zip\n\nunzip -o tshock_${T_VERSION}.zip -d \/mnt\/server",
"container": "alpine:3.9",
"entrypoint": "ash"
}
},
"variables": [
{
"name": "TShock Version",
"description": "Which version of TShock to install and use.",
"env_variable": "T_VERSION",
"default_value": "4.3.22",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|regex:\/^([0-9_\\.-]{5,10})$\/"
},
{
"name": "Maximum Slots",
"description": "Total number of slots to allow on the server.",
"env_variable": "MAX_SLOTS",
"default_value": "20",
"user_viewable": 1,
"user_editable": 0,
"rules": "required|numeric|digits_between:1,3"
}
]
}

View File

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-01-21T17:01:44-06:00", "exported_at": "2020-10-25T22:34:06+00:00",
"name": "Mumble Server", "name": "Mumble Server",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Mumble is an open source, low-latency, high quality voice chat software primarily intended for use while gaming.", "description": "Mumble is an open source, low-latency, high quality voice chat software primarily intended for use while gaming.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\n# Mumble Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add tar curl\n\ncd \/tmp\n\ncurl -sSLO https:\/\/github.com\/mumble-voip\/mumble\/releases\/download\/${MUMBLE_VERSION}\/murmur-static_x86-${MUMBLE_VERSION}.tar.bz2\n\ntar -xjvf murmur-static_x86-${MUMBLE_VERSION}.tar.bz2\ncp -r murmur-static_x86-${MUMBLE_VERSION}\/* \/mnt\/server", "script": "#!\/bin\/bash\r\n# Mumble Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\nGITHUB_PACKAGE=mumble-voip\/mumble\r\nMATCH=murmur-static\r\n\r\napt update\r\napt install -y tar curl jq bzip2\r\n\r\nif [ ! -d \/mnt\/server\/ ]; then\r\n mkdir \/mnt\/server\/\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ -z \"${GITHUB_USER}\" ] && [ -z \"${GITHUB_OAUTH_TOKEN}\" ] ; then\r\n echo -e \"using anon api call\"\r\nelse\r\n echo -e \"user and oauth token set\"\r\n alias curl='curl -u ${GITHUB_USER}:${GITHUB_OAUTH_TOKEN} '\r\nfi\r\n\r\n## get release info and download links\r\nLATEST_JSON=$(curl --silent \"https:\/\/api.github.com\/repos\/${GITHUB_PACKAGE}\/releases\/latest\")\r\nRELEASES=$(curl --silent \"https:\/\/api.github.com\/repos\/${GITHUB_PACKAGE}\/releases\")\r\n\r\nif [ -z \"${VERSION}\" ] || [ \"${VERSION}\" == \"latest\" ]; then\r\n DOWNLOAD_LINK=$(echo ${LATEST_JSON} | jq .assets | jq -r .[].browser_download_url | grep -m 1 -i ${MATCH})\r\nelse\r\n VERSION_CHECK=$(echo ${RELEASES} | jq -r --arg VERSION \"${VERSION}\" '.[] | select(.tag_name==$VERSION) | .tag_name')\r\n if [ \"${VERSION}\" == \"${VERSION_CHECK}\" ]; then\r\n DOWNLOAD_LINK=$(echo ${RELEASES} | jq -r --arg VERSION \"${VERSION}\" '.[] | select(.tag_name==$VERSION) | .assets[].browser_download_url' | grep -m 1 -i ${MATCH})\r\n else\r\n echo -e \"defaulting to latest release\"\r\n DOWNLOAD_LINK=$(echo ${LATEST_JSON} | jq .assets | jq -r .[].browser_download_url)\r\n fi\r\nfi\r\n\r\ncurl -L ${DOWNLOAD_LINK} | tar xjv --strip-components=1",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,17 +28,17 @@
"description": "Maximum concurrent users on the mumble server.", "description": "Maximum concurrent users on the mumble server.",
"env_variable": "MAX_USERS", "env_variable": "MAX_USERS",
"default_value": "100", "default_value": "100",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|numeric|digits_between:1,5" "rules": "required|numeric|digits_between:1,5"
}, },
{ {
"name": "Server Version", "name": "Server Version",
"description": "Version of Mumble Server to download and use.", "description": "Version of Mumble Server to download and use.",
"env_variable": "MUMBLE_VERSION", "env_variable": "MUMBLE_VERSION",
"default_value": "1.3.1", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/" "rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/"
} }
] ]

View File

@ -3,23 +3,23 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-07-05T11:59:29-04:00", "exported_at": "2020-10-25T22:24:15+00:00",
"name": "Teamspeak3 Server", "name": "Teamspeak3 Server",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.", "description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.",
"image": "quay.io/parkervcp/pterodactyl-images:base_debian", "image": "quay.io\/parkervcp\/pterodactyl-images:base_debian",
"startup": "./ts3server default_voice_port={{SERVER_PORT}} query_port={{SERVER_PORT}} filetransfer_ip=0.0.0.0 filetransfer_port={{FILE_TRANSFER}} license_accepted=1", "startup": ".\/ts3server default_voice_port={{SERVER_PORT}} query_port={{SERVER_PORT}} filetransfer_ip=0.0.0.0 filetransfer_port={{FILE_TRANSFER}} license_accepted=1",
"config": { "config": {
"files": "{}", "files": "{}",
"startup": "{\r\n \"done\": \"listening on 0.0.0.0:\",\r\n \"userInteraction\": []\r\n}", "startup": "{\r\n \"done\": \"listening on 0.0.0.0:\",\r\n \"userInteraction\": []\r\n}",
"logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs/ts3.log\"\r\n}", "logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/ts3.log\"\r\n}",
"stop": "^C" "stop": "^C"
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!/bin/ash\r\n# TS3 Installation Script\r\n#\r\n# Server Files: /mnt/server\r\napk add --no-cache tar curl jq\r\n\r\nif [ -z ${TS_VERSION} ] || [ ${TS_VERSION} == latest ]; then\r\n TS_VERSION=$(wget https://teamspeak.com/versions/server.json -qO - | jq -r '.linux.x86_64.version')\r\nfi\r\n\r\ncd /mnt/server\r\n\r\n\r\necho -e \"getting files from http://files.teamspeak-services.com/releases/server/${TS_VERSION}/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2\"\r\ncurl http://files.teamspeak-services.com/releases/server/${TS_VERSION}/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2 | tar xj --strip-components=1", "script": "#!\/bin\/bash\r\n# TS3 Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y tar curl jq bzip2\r\n\r\nif [ -z ${TS_VERSION} ] || [ ${TS_VERSION} == latest ]; then\r\n TS_VERSION=$(curl -sSL https:\/\/teamspeak.com\/versions\/server.json | jq -r '.linux.x86_64.version')\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"getting files from http:\/\/files.teamspeak-services.com\/releases\/server\/${TS_VERSION}\/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2\" \r\ncurl -L http:\/\/files.teamspeak-services.com\/releases\/server\/${TS_VERSION}\/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2 | tar -xvj --strip-components=1\r\n\r\nrm teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The version of Teamspeak 3 to use when running the server.", "description": "The version of Teamspeak 3 to use when running the server.",
"env_variable": "TS_VERSION", "env_variable": "TS_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:6" "rules": "required|string|max:6"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The Teamspeak file transfer port", "description": "The Teamspeak file transfer port",
"env_variable": "FILE_TRANSFER", "env_variable": "FILE_TRANSFER",
"default_value": "30033", "default_value": "30033",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|integer|between:1,65535" "rules": "required|integer|between:1,65535"
} }
] ]

View File

@ -1,9 +1,10 @@
{ {
"name": "pterodactyl-panel", "name": "pterodactyl-panel",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "1.2.19", "@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.9.0", "@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "0.1.4", "@fortawesome/react-fontawesome": "^0.1.11",
"@types/react-copy-to-clipboard": "^4.3.0",
"axios": "^0.19.2", "axios": "^0.19.2",
"chart.js": "^2.8.0", "chart.js": "^2.8.0",
"codemirror": "^5.57.0", "codemirror": "^5.57.0",
@ -21,11 +22,12 @@
"path": "^0.12.7", "path": "^0.12.7",
"query-string": "^6.7.0", "query-string": "^6.7.0",
"react": "^16.13.1", "react": "^16.13.1",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "npm:@hot-loader/react-dom", "react-dom": "npm:@hot-loader/react-dom",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-ga": "^3.1.2",
"react-google-recaptcha": "^2.0.1", "react-google-recaptcha": "^2.0.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-ga": "^3.1.2",
"react-hot-loader": "^4.12.21", "react-hot-loader": "^4.12.21",
"react-i18next": "^11.2.1", "react-i18next": "^11.2.1",
"react-redux": "^7.1.0", "react-redux": "^7.1.0",
@ -37,29 +39,31 @@
"styled-components-breakpoint": "^3.0.0-preview.20", "styled-components-breakpoint": "^3.0.0-preview.20",
"swr": "^0.2.3", "swr": "^0.2.3",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"xterm": "^3.14.4", "xterm": "^4.9.0",
"xterm-addon-attach": "^0.1.0", "xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.1.0", "xterm-addon-fit": "^0.4.0",
"xterm-addon-search": "^0.7.0",
"xterm-addon-search-bar": "^0.2.0",
"yup": "^0.29.1" "yup": "^0.29.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.5", "@babel/core": "^7.12.1",
"@babel/plugin-proposal-class-properties": "^7.7.4", "@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
"@babel/plugin-proposal-object-rest-spread": "^7.7.4", "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.8.3", "@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.7.4", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-react-jsx": "^7.10.4", "@babel/plugin-transform-react-jsx": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.7.5", "@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.7.5", "@babel/preset-env": "^7.12.1",
"@babel/preset-react": "^7.7.4", "@babel/preset-react": "^7.12.1",
"@babel/preset-typescript": "^7.7.4", "@babel/preset-typescript": "^7.12.1",
"@babel/runtime": "^7.7.5", "@babel/runtime": "^7.12.1",
"@types/chart.js": "^2.8.5", "@types/chart.js": "^2.8.5",
"@types/codemirror": "^0.0.98", "@types/codemirror": "^0.0.98",
"@types/debounce": "^1.2.0", "@types/debounce": "^1.2.0",
"@types/events": "^3.0.0", "@types/events": "^3.0.0",
"@types/node": "^12.6.9", "@types/node": "^14.11.10",
"@types/query-string": "^6.3.0", "@types/query-string": "^6.3.0",
"@types/react": "^16.9.41", "@types/react": "^16.9.41",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
@ -91,9 +95,9 @@
"source-map-loader": "^1.0.1", "source-map-loader": "^1.0.1",
"style-loader": "^1.2.1", "style-loader": "^1.2.1",
"svg-url-loader": "^6.0.0", "svg-url-loader": "^6.0.0",
"tailwindcss": "^1.4.6", "tailwindcss": "^1.9.4",
"terser-webpack-plugin": "^3.0.6", "terser-webpack-plugin": "^3.0.6",
"twin.macro": "^1.4.1", "twin.macro": "^1.10.0",
"typescript": "^3.9.6", "typescript": "^3.9.6",
"typescript-plugin-tw-template": "^2.0.1", "typescript-plugin-tw-template": "^2.0.1",
"webpack": "^4.43.0", "webpack": "^4.43.0",

View File

@ -14,7 +14,7 @@ return [
'deleted' => 'Successfully deleted the requested location.', 'deleted' => 'Successfully deleted the requested location.',
], ],
'user' => [ 'user' => [
'search_users' => 'Enter a Username, UUID, or Email Address', 'search_users' => 'Enter a Username, User ID, or Email Address',
'select_search_user' => 'ID of user to delete (Enter \'0\' to re-search)', 'select_search_user' => 'ID of user to delete (Enter \'0\' to re-search)',
'deleted' => 'User successfully deleted from the Panel.', 'deleted' => 'User successfully deleted from the Panel.',
'confirm_delete' => 'Are you sure you want to delete this user from the Panel?', 'confirm_delete' => 'Are you sure you want to delete this user from the Panel?',

View File

@ -4,16 +4,15 @@ import http, { getPaginationSet, PaginatedResult } from '@/api/http';
interface QueryParams { interface QueryParams {
query?: string; query?: string;
page?: number; page?: number;
onlyAdmin?: boolean; type?: string;
} }
export default ({ query, page = 1, onlyAdmin = false }: QueryParams): Promise<PaginatedResult<Server>> => { export default ({ query, ...params }: QueryParams): Promise<PaginatedResult<Server>> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get('/api/client', { http.get('/api/client', {
params: { params: {
type: onlyAdmin ? 'admin' : undefined, 'filter[*]': query,
'filter[name]': query, ...params,
page,
}, },
}) })
.then(({ data }) => resolve({ .then(({ data }) => resolve({

View File

@ -1,18 +1,10 @@
import http from '@/api/http'; import http from '@/api/http';
export default (uuid: string, file: string, content: string): Promise<void> => { export default async (uuid: string, file: string, content: string): Promise<void> => {
return new Promise((resolve, reject) => { await http.post(`/api/client/servers/${uuid}/files/write`, content, {
http.post(
`/api/client/servers/${uuid}/files/write`,
content,
{
params: { file }, params: { file },
headers: { headers: {
'Content-Type': 'text/plain', 'Content-Type': 'text/plain',
}, },
},
)
.then(() => resolve())
.catch(reject);
}); });
}; };

View File

@ -0,0 +1,4 @@
import http from '@/api/http';
export default async (server: string, schedule: number): Promise<void> =>
await http.post(`/api/client/servers/${server}/schedules/${schedule}/execute`);

View File

@ -63,7 +63,7 @@ export default () => {
<FontAwesomeIcon icon={faUserCircle}/> <FontAwesomeIcon icon={faUserCircle}/>
</NavLink> </NavLink>
{rootAdmin && {rootAdmin &&
<a href={'/admin'} target={'_blank'} rel={'noreferrer'}> <a href={'/admin'} rel={'noreferrer'}>
<FontAwesomeIcon icon={faCogs}/> <FontAwesomeIcon icon={faCogs}/>
</a> </a>
} }

View File

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
@ -24,13 +24,23 @@ export default () => {
const { clearFlashes, addFlash } = useFlash(); const { clearFlashes, addFlash } = useFlash();
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha); const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
useEffect(() => {
clearFlashes();
}, []);
const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => { const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
clearFlashes(); clearFlashes();
// If there is no token in the state yet, request the token and then abort this submit request // If there is no token in the state yet, request the token and then abort this submit request
// since it will be re-submitted when the recaptcha data is returned by the component. // since it will be re-submitted when the recaptcha data is returned by the component.
if (recaptchaEnabled && !token) { if (recaptchaEnabled && !token) {
ref.current!.execute().catch(error => console.error(error)); ref.current!.execute().catch(error => {
console.error(error);
setSubmitting(false);
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
});
return; return;
} }
@ -43,7 +53,12 @@ export default () => {
console.error(error); console.error(error);
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
}) })
.then(() => setSubmitting(false)); .then(() => {
setToken('');
if (ref.current) ref.current.reset();
setSubmitting(false);
});
}; };
return ( return (

View File

@ -1,7 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, RouteComponentProps } from 'react-router-dom'; import { Link, RouteComponentProps } from 'react-router-dom';
import loginCheckpoint from '@/api/auth/loginCheckpoint'; import loginCheckpoint from '@/api/auth/loginCheckpoint';
import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer'; import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { ActionCreator } from 'easy-peasy'; import { ActionCreator } from 'easy-peasy';
import { StaticContext } from 'react-router'; import { StaticContext } from 'react-router';
@ -20,8 +19,7 @@ interface Values {
type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }> type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }>
type Props = OwnProps & { type Props = OwnProps & {
addError: ActionCreator<FlashStore['addError']['payload']>; clearAndAddHttpError: ActionCreator<FlashStore['clearAndAddHttpError']['payload']>;
clearFlashes: ActionCreator<FlashStore['clearFlashes']['payload']>;
} }
const LoginCheckpointContainer = () => { const LoginCheckpointContainer = () => {
@ -79,9 +77,7 @@ const LoginCheckpointContainer = () => {
}; };
const EnhancedForm = withFormik<Props, Values>({ const EnhancedForm = withFormik<Props, Values>({
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { addError, clearFlashes, location } }) => { handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { clearAndAddHttpError, location } }) => {
clearFlashes();
loginCheckpoint(location.state?.token || '', code, recoveryCode) loginCheckpoint(location.state?.token || '', code, recoveryCode)
.then(response => { .then(response => {
if (response.complete) { if (response.complete) {
@ -95,7 +91,7 @@ const EnhancedForm = withFormik<Props, Values>({
.catch(error => { .catch(error => {
console.error(error); console.error(error);
setSubmitting(false); setSubmitting(false);
addError({ message: httpErrorToHuman(error) }); clearAndAddHttpError({ error });
}); });
}, },
@ -106,7 +102,7 @@ const EnhancedForm = withFormik<Props, Values>({
})(LoginCheckpointContainer); })(LoginCheckpointContainer);
export default ({ history, location, ...props }: OwnProps) => { export default ({ history, location, ...props }: OwnProps) => {
const { addError, clearFlashes } = useFlash(); const { clearAndAddHttpError } = useFlash();
if (!location.state?.token) { if (!location.state?.token) {
history.replace('/auth/login'); history.replace('/auth/login');
@ -115,8 +111,7 @@ export default ({ history, location, ...props }: OwnProps) => {
} }
return <EnhancedForm return <EnhancedForm
addError={addError} clearAndAddHttpError={clearAndAddHttpError}
clearFlashes={clearFlashes}
history={history} history={history}
location={location} location={location}
{...props} {...props}

View File

@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Link, RouteComponentProps } from 'react-router-dom'; import { Link, RouteComponentProps } from 'react-router-dom';
import login from '@/api/auth/login'; import login from '@/api/auth/login';
import LoginFormContainer from '@/components/auth/LoginFormContainer'; import LoginFormContainer from '@/components/auth/LoginFormContainer';
@ -23,13 +23,23 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
const { clearFlashes, clearAndAddHttpError } = useFlash(); const { clearFlashes, clearAndAddHttpError } = useFlash();
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha); const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
useEffect(() => {
clearFlashes();
}, []);
const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => { const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes(); clearFlashes();
// If there is no token in the state yet, request the token and then abort this submit request // If there is no token in the state yet, request the token and then abort this submit request
// since it will be re-submitted when the recaptcha data is returned by the component. // since it will be re-submitted when the recaptcha data is returned by the component.
if (recaptchaEnabled && !token) { if (recaptchaEnabled && !token) {
ref.current!.execute().catch(error => console.error(error)); ref.current!.execute().catch(error => {
console.error(error);
setSubmitting(false);
clearAndAddHttpError({ error });
});
return; return;
} }
@ -46,6 +56,9 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
.catch(error => { .catch(error => {
console.error(error); console.error(error);
setToken('');
if (ref.current) ref.current.reset();
setSubmitting(false); setSubmitting(false);
clearAndAddHttpError({ error }); clearAndAddHttpError({ error });
}); });
@ -63,23 +76,23 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
{({ isSubmitting, setSubmitting, submitForm }) => ( {({ isSubmitting, setSubmitting, submitForm }) => (
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}> <LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}>
<Field <Field
light
type={'text'} type={'text'}
label={'Username or Email'} label={'Username or Email'}
id={'username'}
name={'username'} name={'username'}
light disabled={isSubmitting}
/> />
<div css={tw`mt-6`}> <div css={tw`mt-6`}>
<Field <Field
light
type={'password'} type={'password'}
label={'Password'} label={'Password'}
id={'password'}
name={'password'} name={'password'}
light disabled={isSubmitting}
/> />
</div> </div>
<div css={tw`mt-6`}> <div css={tw`mt-6`}>
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}> <Button type={'submit'} size={'xlarge'} isLoading={isSubmitting} disabled={isSubmitting}>
Login Login
</Button> </Button>
</div> </div>

View File

@ -21,7 +21,7 @@ export default () => {
const { data: servers, error } = useSWR<PaginatedResult<Server>>( const { data: servers, error } = useSWR<PaginatedResult<Server>>(
[ '/api/client/servers', showOnlyAdmin, page ], [ '/api/client/servers', showOnlyAdmin, page ],
() => getServers({ onlyAdmin: showOnlyAdmin, page }), () => getServers({ page, type: showOnlyAdmin ? 'admin' : undefined }),
); );
useEffect(() => { useEffect(() => {

View File

@ -41,8 +41,9 @@ const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | unde
export default ({ server, className }: { server: Server; className?: string }) => { export default ({ server, className }: { server: Server; className?: string }) => {
const interval = useRef<number>(null); const interval = useRef<number>(null);
const [ stats, setStats ] = useState<ServerStats | null>(null);
const [ statsError, setStatsError ] = useState(false); const [ statsError, setStatsError ] = useState(false);
const [ isSuspended, setIsSuspended ] = useState(server.isSuspended);
const [ stats, setStats ] = useState<ServerStats | null>(null);
const getStats = () => { const getStats = () => {
setStatsError(false); setStatsError(false);
@ -55,6 +56,14 @@ export default ({ server, className }: { server: Server; className?: string }) =
}; };
useEffect(() => { useEffect(() => {
setIsSuspended(stats?.isSuspended || server.isSuspended);
}, [ stats?.isSuspended, server.isSuspended ]);
useEffect(() => {
// Don't waste a HTTP request if there is nothing important to show to the user because
// the server is suspended.
if (isSuspended) return;
getStats().then(() => { getStats().then(() => {
// @ts-ignore // @ts-ignore
interval.current = setInterval(() => getStats(), 20000); interval.current = setInterval(() => getStats(), 20000);
@ -63,7 +72,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
return () => { return () => {
interval.current && clearInterval(interval.current); interval.current && clearInterval(interval.current);
}; };
}, []); }, [ isSuspended ]);
const alarms = { cpu: false, memory: false, disk: false }; const alarms = { cpu: false, memory: false, disk: false };
if (stats) { if (stats) {
@ -101,9 +110,13 @@ export default ({ server, className }: { server: Server; className?: string }) =
</p> </p>
</div> </div>
<div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}> <div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}>
{!stats ? {(!stats || isSuspended) ?
!statsError ? isSuspended ?
<Spinner size={'small'}/> <div css={tw`flex-1 text-center`}>
<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
{server.isSuspended ? 'Suspended' : 'Connection Error'}
</span>
</div>
: :
server.isInstalling ? server.isInstalling ?
<div css={tw`flex-1 text-center`}> <div css={tw`flex-1 text-center`}>
@ -112,11 +125,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
</span> </span>
</div> </div>
: :
<div css={tw`flex-1 text-center`}> <Spinner size={'small'}/>
<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
{server.isSuspended ? 'Suspended' : 'Connection Error'}
</span>
</div>
: :
<React.Fragment> <React.Fragment>
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}> <div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>

View File

@ -7,7 +7,6 @@ import { object, string } from 'yup';
import { Actions, useStoreActions } from 'easy-peasy'; import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor'; import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
import { httpErrorToHuman } from '@/api/http';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
@ -16,11 +15,10 @@ interface Values {
} }
export default ({ ...props }: RequiredModalProps) => { export default ({ ...props }: RequiredModalProps) => {
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData); const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
const submit = ({ password }: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = ({ password }: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('account:two-factor');
disableAccountTwoFactor(password) disableAccountTwoFactor(password)
.then(() => { .then(() => {
updateUserData({ useTotp: false }); updateUserData({ useTotp: false });
@ -29,7 +27,7 @@ export default ({ ...props }: RequiredModalProps) => {
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' }); clearAndAddHttpError({ error, key: 'account:two-factor' });
setSubmitting(false); setSubmitting(false);
}); });
}; };

View File

@ -6,7 +6,6 @@ import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor'; import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
import { Actions, useStoreActions } from 'easy-peasy'; import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import tw from 'twin.macro'; import tw from 'twin.macro';
@ -22,20 +21,18 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]); const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]);
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData); const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
useEffect(() => { useEffect(() => {
clearFlashes('account:two-factor');
getTwoFactorTokenUrl() getTwoFactorTokenUrl()
.then(setToken) .then(setToken)
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' }); clearAndAddHttpError({ error, key: 'account:two-factor' });
}); });
}, []); }, []);
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('account:two-factor');
enableAccountTwoFactor(code) enableAccountTwoFactor(code)
.then(tokens => { .then(tokens => {
setRecoveryTokens(tokens); setRecoveryTokens(tokens);
@ -43,7 +40,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' }); clearAndAddHttpError({ error, key: 'account:two-factor' });
}) })
.then(() => setSubmitting(false)); .then(() => setSubmitting(false));
}; };

View File

@ -9,7 +9,6 @@ import InputSpinner from '@/components/elements/InputSpinner';
import getServers from '@/api/getServers'; import getServers from '@/api/getServers';
import { Server } from '@/api/server/getServer'; import { Server } from '@/api/server/getServer';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import tw from 'twin.macro'; import tw from 'twin.macro';
@ -47,55 +46,52 @@ const SearchWatcher = () => {
export default ({ ...props }: Props) => { export default ({ ...props }: Props) => {
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
const [ loading, setLoading ] = useState(false);
const [ servers, setServers ] = useState<Server[]>([]);
const isAdmin = useStoreState(state => state.user.data!.rootAdmin); const isAdmin = useStoreState(state => state.user.data!.rootAdmin);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const [ servers, setServers ] = useState<Server[]>([]);
const { clearAndAddHttpError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const search = debounce(({ term }: Values, { setSubmitting }: FormikHelpers<Values>) => { const search = debounce(({ term }: Values, { setSubmitting }: FormikHelpers<Values>) => {
setLoading(true);
setSubmitting(false);
clearFlashes('search'); clearFlashes('search');
getServers({ query: term }) // if (ref.current) ref.current.focus();
getServers({ query: term, type: isAdmin ? 'admin-all' : undefined })
.then(servers => setServers(servers.items.filter((_, index) => index < 5))) .then(servers => setServers(servers.items.filter((_, index) => index < 5)))
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ key: 'search', message: httpErrorToHuman(error) }); clearAndAddHttpError({ key: 'search', error });
}) })
.then(() => setLoading(false)); .then(() => setSubmitting(false))
.then(() => ref.current?.focus());
}, 500); }, 500);
useEffect(() => { useEffect(() => {
if (props.visible) { if (props.visible) {
setTimeout(() => ref.current?.focus(), 250); if (ref.current) ref.current.focus();
} }
}, [ props.visible ]); }, [ props.visible ]);
// Formik does not support an innerRef on custom components.
const InputWithRef = (props: any) => <Input autoFocus {...props} ref={ref}/>;
return ( return (
<Formik <Formik
onSubmit={search} onSubmit={search}
validationSchema={object().shape({ validationSchema={object().shape({
term: string() term: string().min(3, 'Please enter at least three characters to begin searching.'),
.min(3, 'Please enter at least three characters to begin searching.')
.required('A search term must be provided.'),
})} })}
initialValues={{ term: '' } as Values} initialValues={{ term: '' } as Values}
> >
{({ isSubmitting }) => (
<Modal {...props}> <Modal {...props}>
<Form> <Form>
<FormikFieldWrapper <FormikFieldWrapper
name={'term'} name={'term'}
label={'Search term'} label={'Search term'}
description={ description={'Enter a server name, uuid, or allocation to begin searching.'}
isAdmin
? 'Enter a server name, user email, or uuid to begin searching.'
: 'Enter a server name to begin searching.'
}
> >
<SearchWatcher/> <SearchWatcher/>
<InputSpinner visible={loading}> <InputSpinner visible={isSubmitting}>
<Field as={Input} innerRef={ref} name={'term'}/> <Field as={InputWithRef} name={'term'}/>
</InputSpinner> </InputSpinner>
</FormikFieldWrapper> </FormikFieldWrapper>
</Form> </Form>
@ -108,7 +104,7 @@ export default ({ ...props }: Props) => {
to={`/server/${server.id}`} to={`/server/${server.id}`}
onClick={() => props.onDismissed()} onClick={() => props.onDismissed()}
> >
<div> <div css={tw`flex-1 mr-4`}>
<p css={tw`text-sm`}>{server.name}</p> <p css={tw`text-sm`}>{server.name}</p>
<p css={tw`mt-1 text-xs text-neutral-400`}> <p css={tw`mt-1 text-xs text-neutral-400`}>
{ {
@ -118,7 +114,7 @@ export default ({ ...props }: Props) => {
} }
</p> </p>
</div> </div>
<div css={tw`flex-1 text-right`}> <div css={tw`flex-none text-right`}>
<span css={tw`text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded`}> <span css={tw`text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded`}>
{server.node} {server.node}
</span> </span>
@ -129,6 +125,7 @@ export default ({ ...props }: Props) => {
</div> </div>
} }
</Modal> </Modal>
)}
</Formik> </Formik>
); );
}; };

View File

@ -57,8 +57,8 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
`}; `};
`}; `};
${props => props.size === 'xsmall' && tw`p-2 text-xs`}; ${props => props.size === 'xsmall' && tw`px-2 py-1 text-xs`};
${props => (!props.size || props.size === 'small') && tw`p-3`}; ${props => (!props.size || props.size === 'small') && tw`px-4 py-2`};
${props => props.size === 'large' && tw`p-4 text-sm`}; ${props => props.size === 'large' && tw`p-4 text-sm`};
${props => props.size === 'xlarge' && tw`p-4 w-full`}; ${props => props.size === 'xlarge' && tw`p-4 w-full`};

View File

@ -0,0 +1,63 @@
import React, { useCallback, useEffect, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import tw from 'twin.macro';
import styled, { keyframes } from 'styled-components/macro';
import Fade from '@/components/elements/Fade';
import { SwitchTransition } from 'react-transition-group';
const fade = keyframes`
from { opacity: 0 }
to { opacity: 1 }
`;
const Toast = styled.div`
${tw`fixed z-50 bottom-0 left-0 mb-4 w-full flex justify-end pr-4`};
animation: ${fade} 250ms linear;
& > div {
${tw`rounded px-4 py-2 text-white bg-neutral-800 border border-neutral-900`};
}
`;
const CopyOnClick: React.FC<{ text: string }> = ({ text, children }) => {
const [ copied, setCopied ] = useState(false);
useEffect(() => {
if (!copied) return;
const timeout = setTimeout(() => {
setCopied(false);
}, 2500);
return () => {
clearTimeout(timeout);
};
}, [ copied ]);
const onCopy = useCallback(() => {
setCopied(true);
}, []);
return (
<>
<SwitchTransition>
<Fade timeout={250} key={copied ? 'visible' : 'invisible'}>
{copied ?
<Toast>
<div>
<p>Copied &quot;{text}&quot; to clipboard.</p>
</div>
</Toast>
:
<></>
}
</Fade>
</SwitchTransition>
<CopyToClipboard onCopy={onCopy} text={text} options={{ debug: true }}>
{children}
</CopyToClipboard>
</>
);
};
export default CopyOnClick;

View File

@ -0,0 +1,39 @@
import React from 'react';
import tw from 'twin.macro';
import Icon from '@/components/elements/Icon';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
interface State {
hasError: boolean;
}
// eslint-disable-next-line @typescript-eslint/ban-types
class ErrorBoundary extends React.Component<{}, State> {
state: State = {
hasError: false,
};
static getDerivedStateFromError () {
return { hasError: true };
}
componentDidCatch (error: Error) {
console.error(error);
}
render () {
return this.state.hasError ?
<div css={tw`flex items-center justify-center w-full my-4`}>
<div css={tw`flex items-center bg-neutral-900 rounded p-3 text-red-500`}>
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`}/>
<p css={tw`text-sm text-neutral-100`}>
An error was encountered by the application while rendering this view. Try refreshing the page.
</p>
</div>
</div>
:
this.props.children;
}
}
export default ErrorBoundary;

View File

@ -0,0 +1,31 @@
import React, { CSSProperties } from 'react';
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import tw from 'twin.macro';
interface Props {
icon: IconDefinition;
className?: string;
style?: CSSProperties;
}
const Icon = ({ icon, className, style }: Props) => {
let [ width, height, , , paths ] = icon.icon;
paths = Array.isArray(paths) ? paths : [ paths ];
return (
<svg
xmlns={'http://www.w3.org/2000/svg'}
viewBox={`0 0 ${width} ${height}`}
css={tw`fill-current inline-block`}
className={className}
style={style}
>
{paths.map((path, index) => (
<path key={`svg_path_${index}`} d={path}/>
))}
</svg>
);
};
export default Icon;

View File

@ -5,12 +5,7 @@ import tw from 'twin.macro';
const InputSpinner = ({ visible, children }: { visible: boolean, children: React.ReactNode }) => ( const InputSpinner = ({ visible, children }: { visible: boolean, children: React.ReactNode }) => (
<div css={tw`relative`}> <div css={tw`relative`}>
<Fade <Fade appear unmountOnExit in={visible} timeout={150}>
appear
unmountOnExit
in={visible}
timeout={150}
>
<div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}> <div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}>
<Spinner size={'small'}/> <Spinner size={'small'}/>
</div> </div>

View File

@ -1,9 +1,10 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import tw from 'twin.macro'; import tw from 'twin.macro';
import styled, { css } from 'styled-components/macro'; import styled, { css } from 'styled-components/macro';
import { breakpoint } from '@/theme'; import { breakpoint } from '@/theme';
import Fade from '@/components/elements/Fade'; import Fade from '@/components/elements/Fade';
import { createPortal } from 'react-dom';
export interface RequiredModalProps { export interface RequiredModalProps {
visible: boolean; visible: boolean;
@ -124,4 +125,10 @@ const Modal: React.FC<ModalProps> = ({ visible, appear, dismissable, showSpinner
); );
}; };
export default Modal; const PortaledModal: React.FC<ModalProps> = ({ children, ...props }) => {
const element = useRef(document.getElementById('modal-portal'));
return createPortal(<Modal {...props}>{children}</Modal>, element.current!);
};
export default PortaledModal;

View File

@ -1,19 +1,22 @@
import React, { useEffect, useMemo, useRef } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ITerminalOptions, Terminal } from 'xterm'; import { ITerminalOptions, Terminal } from 'xterm';
import * as TerminalFit from 'xterm/lib/addons/fit/fit'; import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
import { SearchBarAddon } from 'xterm-addon-search-bar';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import { usePermissions } from '@/plugins/usePermissions'; import { usePermissions } from '@/plugins/usePermissions';
import tw from 'twin.macro'; import tw, { theme as th } from 'twin.macro';
import 'xterm/dist/xterm.css'; import 'xterm/css/xterm.css';
import useEventListener from '@/plugins/useEventListener'; import useEventListener from '@/plugins/useEventListener';
import { debounce } from 'debounce'; import { debounce } from 'debounce';
import { usePersistedState } from '@/plugins/usePersistedState';
const theme = { const theme = {
background: 'transparent', background: th`colors.black`.toString(),
cursor: 'transparent', cursor: 'transparent',
black: '#000000', black: th`colors.black`.toString(),
red: '#E54B4B', red: '#E54B4B',
green: '#9ECE58', green: '#9ECE58',
yellow: '#FAED70', yellow: '#FAED70',
@ -29,6 +32,7 @@ const theme = {
brightMagenta: '#C792EA', brightMagenta: '#C792EA',
brightCyan: '#89DDFF', brightCyan: '#89DDFF',
brightWhite: '#ffffff', brightWhite: '#ffffff',
selection: '#FAF089',
}; };
const terminalProps: ITerminalOptions = { const terminalProps: ITerminalOptions = {
@ -55,8 +59,14 @@ export default () => {
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pterodactyl~ \u001b[0m'; const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pterodactyl~ \u001b[0m';
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const terminal = useMemo(() => new Terminal({ ...terminalProps }), []); const terminal = useMemo(() => new Terminal({ ...terminalProps }), []);
const fitAddon = new FitAddon();
const searchAddon = new SearchAddon();
const searchBar = new SearchBarAddon({ searchAddon });
const { connected, instance } = ServerContext.useStoreState(state => state.socket); const { connected, instance } = ServerContext.useStoreState(state => state.socket);
const [ canSendCommands ] = usePermissions([ 'control.console' ]); const [ canSendCommands ] = usePermissions([ 'control.console' ]);
const serverId = ServerContext.useStoreState(state => state.server.data!.id);
const [ history, setHistory ] = usePersistedState<string[]>(`${serverId}:command_history`, []);
const [ historyIndex, setHistoryIndex ] = useState(-1);
const handleConsoleOutput = (line: string, prelude = false) => terminal.writeln( const handleConsoleOutput = (line: string, prelude = false) => terminal.writeln(
(prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m', (prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
@ -71,37 +81,61 @@ export default () => {
); );
const handleCommandKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleCommandKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter' || (e.key === 'Enter' && e.currentTarget.value.length < 1)) { if (e.key === 'ArrowUp') {
return; const newIndex = Math.min(historyIndex + 1, history!.length - 1);
setHistoryIndex(newIndex);
e.currentTarget.value = history![newIndex] || '';
} }
instance && instance.send('send command', e.currentTarget.value); if (e.key === 'ArrowDown') {
const newIndex = Math.max(historyIndex - 1, -1);
setHistoryIndex(newIndex);
e.currentTarget.value = history![newIndex] || '';
}
const command = e.currentTarget.value;
if (e.key === 'Enter' && command.length > 0) {
setHistory(prevHistory => [ command, ...prevHistory! ].slice(0, 32));
setHistoryIndex(-1);
instance && instance.send('send command', command);
e.currentTarget.value = ''; e.currentTarget.value = '';
}
}; };
useEffect(() => { useEffect(() => {
if (connected && ref.current && !terminal.element) { if (connected && ref.current && !terminal.element) {
terminal.open(ref.current); terminal.open(ref.current);
terminal.loadAddon(fitAddon);
terminal.loadAddon(searchAddon);
terminal.loadAddon(searchBar);
fitAddon.fit();
// @see https://github.com/xtermjs/xterm.js/issues/2265 // Add support for capturing keys
// @see https://github.com/xtermjs/xterm.js/issues/2230
TerminalFit.fit(terminal);
// Add support for copying terminal text.
terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => { terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
// Ctrl + C // Ctrl + C ( Copy )
if (e.ctrlKey && (e.key === 'c')) { if (e.ctrlKey && e.key === 'c') {
document.execCommand('copy'); document.execCommand('copy');
return false; return false;
} }
if (e.ctrlKey && e.key === 'f') {
searchBar.show();
return false;
}
if (e.key === 'Escape') {
searchBar.hidden();
}
return true; return true;
}); });
} }
}, [ terminal, connected ]); }, [ terminal, connected ]);
const fit = debounce(() => { const fit = debounce(() => {
TerminalFit.fit(terminal); fitAddon.fit();
}, 100); }, 100);
useEventListener('resize', () => fit()); useEventListener('resize', () => fit());

View File

@ -16,6 +16,7 @@ import Select from '@/components/elements/Select';
import modes from '@/modes'; import modes from '@/modes';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import ErrorBoundary from '@/components/elements/ErrorBoundary';
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor')); const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
@ -60,9 +61,7 @@ export default () => {
setLoading(true); setLoading(true);
clearFlashes('files:view'); clearFlashes('files:view');
fetchFileContent() fetchFileContent()
.then(content => { .then(content => saveFileContents(uuid, encodeURIComponent(name || hash.replace(/^#/, '')), content))
return saveFileContents(uuid, name || hash.replace(/^#/, ''), content);
})
.then(() => { .then(() => {
if (name) { if (name) {
history.push(`/server/${id}/files/edit#/${name}`); history.push(`/server/${id}/files/edit#/${name}`);
@ -87,7 +86,9 @@ export default () => {
return ( return (
<PageContentBlock> <PageContentBlock>
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/> <FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
<ErrorBoundary>
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/> <FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
</ErrorBoundary>
{hash.replace(/^#/, '').endsWith('.pteroignore') && {hash.replace(/^#/, '').endsWith('.pteroignore') &&
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}> <div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
<p css={tw`text-neutral-300 text-sm`}> <p css={tw`text-neutral-300 text-sm`}>

View File

@ -33,10 +33,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
.filter(directory => !!directory) .filter(directory => !!directory)
.map((directory, index, dirs) => { .map((directory, index, dirs) => {
if (!withinFileEditor && index === dirs.length - 1) { if (!withinFileEditor && index === dirs.length - 1) {
return { name: decodeURIComponent(directory) }; return { name: decodeURIComponent(encodeURIComponent(directory)) };
} }
return { name: decodeURIComponent(directory), path: `/${dirs.slice(0, index + 1).join('/')}` }; return { name: decodeURIComponent(encodeURIComponent(directory)), path: `/${dirs.slice(0, index + 1).join('/')}` };
}); });
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => { const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
} }
{file && {file &&
<React.Fragment> <React.Fragment>
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(file)}</span> <span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(encodeURIComponent(file))}</span>
</React.Fragment> </React.Fragment>
} }
</div> </div>

View File

@ -17,6 +17,7 @@ import MassActionsBar from '@/components/server/files/MassActionsBar';
import UploadButton from '@/components/server/files/UploadButton'; import UploadButton from '@/components/server/files/UploadButton';
import ServerContentBlock from '@/components/elements/ServerContentBlock'; import ServerContentBlock from '@/components/elements/ServerContentBlock';
import { useStoreActions } from '@/state/hooks'; import { useStoreActions } from '@/state/hooks';
import ErrorBoundary from '@/components/elements/ErrorBoundary';
const sortFiles = (files: FileObject[]): FileObject[] => { const sortFiles = (files: FileObject[]): FileObject[] => {
return files.sort((a, b) => a.name.localeCompare(b.name)) return files.sort((a, b) => a.name.localeCompare(b.name))
@ -50,7 +51,9 @@ export default () => {
return ( return (
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}> <ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
<ErrorBoundary>
<FileManagerBreadcrumbs/> <FileManagerBreadcrumbs/>
</ErrorBoundary>
{ {
!files ? !files ?
<Spinner size={'large'} centered/> <Spinner size={'large'} centered/>
@ -81,6 +84,7 @@ export default () => {
</CSSTransition> </CSSTransition>
} }
<Can action={'file.create'}> <Can action={'file.create'}>
<ErrorBoundary>
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}> <div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/> <NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/> <UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
@ -93,6 +97,7 @@ export default () => {
</Button> </Button>
</NavLink> </NavLink>
</div> </div>
</ErrorBoundary>
</Can> </Can>
</> </>
} }

View File

@ -92,9 +92,9 @@ export default ({ className }: WithClassname) => {
<span css={tw`text-neutral-200`}>This directory will be created as</span> <span css={tw`text-neutral-200`}>This directory will be created as</span>
&nbsp;/home/container/ &nbsp;/home/container/
<span css={tw`text-cyan-200`}> <span css={tw`text-cyan-200`}>
{decodeURIComponent( {decodeURIComponent(encodeURIComponent(
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''), join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
)} ))}
</span> </span>
</p> </p>
<div css={tw`flex justify-end`}> <div css={tw`flex justify-end`}>

View File

@ -14,6 +14,7 @@ import { debounce } from 'debounce';
import setServerAllocationNotes from '@/api/server/network/setServerAllocationNotes'; import setServerAllocationNotes from '@/api/server/network/setServerAllocationNotes';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import CopyOnClick from '@/components/elements/CopyOnClick';
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm inline-block`}`; const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm inline-block`}`;
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`; const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
@ -43,11 +44,12 @@ const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
<GreyRowBox $hoverable={false} css={tw`flex-wrap md:flex-no-wrap mt-2`}> <GreyRowBox $hoverable={false} css={tw`flex-wrap md:flex-no-wrap mt-2`}>
<div css={tw`flex items-center w-full md:w-auto`}> <div css={tw`flex items-center w-full md:w-auto`}>
<div css={tw`pl-4 pr-6 text-neutral-400`}> <div css={tw`pl-4 pr-6 text-neutral-400`}>
<FontAwesomeIcon icon={faNetworkWired}/> <FontAwesomeIcon icon={faNetworkWired} />
</div> </div>
<div css={tw`mr-4 flex-1 md:w-40`}> <div css={tw`mr-4 flex-1 md:w-40`}>
<Code>{allocation.alias || allocation.ip}</Code> {allocation.alias ? <CopyOnClick text={allocation.alias}><Code css={tw`w-40 truncate`}>{allocation.alias}</Code></CopyOnClick> :
<Label>IP Address</Label> <CopyOnClick text={allocation.ip}><Code>{allocation.ip}</Code></CopyOnClick>}
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
</div> </div>
<div css={tw`w-16 md:w-24 overflow-hidden`}> <div css={tw`w-16 md:w-24 overflow-hidden`}>
<Code>{allocation.port}</Code> <Code>{allocation.port}</Code>

View File

@ -49,7 +49,7 @@ export default ({ scheduleId, onDeleted }: Props) => {
Are you sure you want to delete this schedule? All tasks will be removed and any running processes Are you sure you want to delete this schedule? All tasks will be removed and any running processes
will be terminated. will be terminated.
</ConfirmationModal> </ConfirmationModal>
<Button css={tw`mr-4`} color={'red'} isSecondary onClick={() => setVisible(true)}> <Button css={tw`flex-1 sm:flex-none mr-4 border-transparent`} color={'red'} isSecondary onClick={() => setVisible(true)}>
Delete Delete
</Button> </Button>
</> </>

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Schedule } from '@/api/server/schedules/getServerSchedules'; import { Schedule } from '@/api/server/schedules/getServerSchedules';
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal'; import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import tw from 'twin.macro';
interface Props { interface Props {
schedule: Schedule; schedule: Schedule;
@ -18,7 +19,7 @@ export default ({ schedule }: Props) => {
onDismissed={() => setVisible(false)} onDismissed={() => setVisible(false)}
/> />
} }
<Button onClick={() => setVisible(true)}> <Button onClick={() => setVisible(true)} css={tw`flex-1`}>
New Task New Task
</Button> </Button>
</> </>

View File

@ -0,0 +1,48 @@
import React, { useCallback, useState } from 'react';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import triggerScheduleExecution from '@/api/server/schedules/triggerScheduleExecution';
import { ServerContext } from '@/state/server';
import useFlash from '@/plugins/useFlash';
import { Schedule } from '@/api/server/schedules/getServerSchedules';
const RunScheduleButton = ({ schedule }: { schedule: Schedule }) => {
const [ loading, setLoading ] = useState(false);
const { clearFlashes, clearAndAddHttpError } = useFlash();
const id = ServerContext.useStoreState(state => state.server.data!.id);
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
const onTriggerExecute = useCallback(() => {
clearFlashes('schedule');
setLoading(true);
triggerScheduleExecution(id, schedule.id)
.then(() => {
setLoading(false);
appendSchedule({ ...schedule, isProcessing: true });
})
.catch(error => {
console.error(error);
clearAndAddHttpError({ error, key: 'schedules' });
})
.then(() => setLoading(false));
}, []);
return (
<>
<SpinnerOverlay visible={loading} size={'large'}/>
<Button
isSecondary
color={'grey'}
css={tw`flex-1 sm:flex-none border-transparent`}
disabled={schedule.isProcessing}
onClick={onTriggerExecute}
>
Run Now
</Button>
</>
);
};
export default RunScheduleButton;

View File

@ -0,0 +1,35 @@
import React from 'react';
import tw from 'twin.macro';
import { Schedule } from '@/api/server/schedules/getServerSchedules';
interface Props {
cron: Schedule['cron'];
className?: string;
}
const ScheduleCronRow = ({ cron, className }: Props) => (
<div css={tw`flex`} className={className}>
<div css={tw`w-1/5 sm:w-auto text-center`}>
<p css={tw`font-medium`}>{cron.minute}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Minute</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{cron.hour}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Hour</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{cron.dayOfMonth}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Day (Month)</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>*</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Month</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{cron.dayOfWeek}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Day (Week)</p>
</div>
</div>
);
export default ScheduleCronRow;

View File

@ -1,12 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom'; import { RouteComponentProps } from 'react-router-dom';
import { Schedule } from '@/api/server/schedules/getServerSchedules'; import { Schedule } from '@/api/server/schedules/getServerSchedules';
import getServerSchedule from '@/api/server/schedules/getServerSchedule'; import getServerSchedule from '@/api/server/schedules/getServerSchedule';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import { httpErrorToHuman } from '@/api/http';
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow';
import EditScheduleModal from '@/components/server/schedules/EditScheduleModal'; import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
import NewTaskButton from '@/components/server/schedules/NewTaskButton'; import NewTaskButton from '@/components/server/schedules/NewTaskButton';
import DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton'; import DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton';
@ -16,7 +13,11 @@ import { ServerContext } from '@/state/server';
import PageContentBlock from '@/components/elements/PageContentBlock'; import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import GreyRowBox from '@/components/elements/GreyRowBox'; import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow';
import isEqual from 'react-fast-compare';
import { format } from 'date-fns';
import ScheduleCronRow from '@/components/server/schedules/ScheduleCronRow';
import RunScheduleButton from '@/components/server/schedules/RunScheduleButton';
interface Params { interface Params {
id: string; id: string;
@ -26,15 +27,34 @@ interface State {
schedule?: Schedule; schedule?: Schedule;
} }
const CronBox = ({ title, value }: { title: string; value: string }) => (
<div css={tw`bg-neutral-700 rounded p-4`}>
<p css={tw`text-neutral-300 text-sm`}>{title}</p>
<p css={tw`text-2xl font-medium text-neutral-100`}>{value}</p>
</div>
);
const ActivePill = ({ active }: { active: boolean }) => (
<span
css={[
tw`rounded-full px-2 py-px text-xs ml-4 uppercase`,
active ? tw`bg-green-600 text-green-100` : tw`bg-red-600 text-red-100`,
]}
>
{active ? 'Active' : 'Inactive'}
</span>
);
export default ({ match, history, location: { state } }: RouteComponentProps<Params, Record<string, unknown>, State>) => { export default ({ match, history, location: { state } }: RouteComponentProps<Params, Record<string, unknown>, State>) => {
const id = ServerContext.useStoreState(state => state.server.data!.id); const id = ServerContext.useStoreState(state => state.server.data!.id);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const { clearFlashes, addError } = useFlash(); const { clearFlashes, clearAndAddHttpError } = useFlash();
const [ isLoading, setIsLoading ] = useState(true); const [ isLoading, setIsLoading ] = useState(true);
const [ showEditModal, setShowEditModal ] = useState(false); const [ showEditModal, setShowEditModal ] = useState(false);
const schedule = ServerContext.useStoreState(st => st.schedules.data.find(s => s.id === state.schedule?.id), [ match ]); // @ts-ignore
const schedule: Schedule | undefined = ServerContext.useStoreState(st => st.schedules.data.find(s => s.id === state.schedule?.id), isEqual);
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule); const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
useEffect(() => { useEffect(() => {
@ -48,11 +68,15 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
.then(schedule => appendSchedule(schedule)) .then(schedule => appendSchedule(schedule))
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ message: httpErrorToHuman(error), key: 'schedules' }); clearAndAddHttpError({ error, key: 'schedules' });
}) })
.then(() => setIsLoading(false)); .then(() => setIsLoading(false));
}, [ match ]); }, [ match ]);
const toggleEditModal = useCallback(() => {
setShowEditModal(s => !s);
}, []);
return ( return (
<PageContentBlock> <PageContentBlock>
<FlashMessageRender byKey={'schedules'} css={tw`mb-4`}/> <FlashMessageRender byKey={'schedules'} css={tw`mb-4`}/>
@ -60,52 +84,73 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
<Spinner size={'large'} centered/> <Spinner size={'large'} centered/>
: :
<> <>
<GreyRowBox css={tw`cursor-pointer mb-2 flex-wrap`}> <ScheduleCronRow cron={schedule.cron} css={tw`sm:hidden bg-neutral-700 rounded mb-4 p-3`}/>
<ScheduleRow schedule={schedule}/> <div css={tw`hidden sm:grid grid-cols-5 md:grid-cols-7 gap-4 mb-6`}>
</GreyRowBox> <CronBox title={'Minute'} value={schedule.cron.minute}/>
<EditScheduleModal <CronBox title={'Hour'} value={schedule.cron.hour}/>
visible={showEditModal} <CronBox title={'Day (Month)'} value={schedule.cron.dayOfMonth}/>
schedule={schedule} <CronBox title={'Month'} value={'*'}/>
onDismissed={() => setShowEditModal(false)} <CronBox title={'Day (Week)'} value={schedule.cron.dayOfWeek}/>
/> </div>
<div css={tw`flex items-center mt-8 mb-4`}> <div css={tw`rounded shadow`}>
<div css={tw`sm:flex items-center bg-neutral-900 p-3 sm:p-6 border-b-4 border-neutral-600 rounded-t`}>
<div css={tw`flex-1`}> <div css={tw`flex-1`}>
<h2 css={tw`text-2xl`}>Configured Tasks</h2> <h3 css={tw`flex items-center text-neutral-100 text-2xl`}>
</div> {schedule.name}
</div> {schedule.isProcessing ?
{schedule.tasks.length > 0 ? <span
<> css={tw`flex items-center rounded-full px-2 py-px text-xs ml-4 uppercase bg-neutral-600 text-white`}
{ >
schedule.tasks <Spinner css={tw`w-3! h-3! mr-2`}/>
.sort((a, b) => a.sequenceId - b.sequenceId) Processing
.map(task => ( </span>
<ScheduleTaskRow key={task.id} task={task} schedule={schedule}/>
))
}
{schedule.tasks.length > 1 &&
<p css={tw`text-xs text-neutral-400`}>
Task delays are relative to the previous task in the listing.
</p>
}
</>
: :
<p css={tw`text-sm text-neutral-400`}> <ActivePill active={schedule.isActive}/>
There are no tasks configured for this schedule.
</p>
} }
<div css={tw`mt-8 flex justify-end`}> </h3>
<p css={tw`mt-1 text-sm text-neutral-300`}>
Last run at:&nbsp;
{schedule.lastRunAt ? format(schedule.lastRunAt, 'MMM do \'at\' h:mma') : 'never'}
</p>
</div>
<div css={tw`flex sm:block mt-3 sm:mt-0`}>
<Can action={'schedule.update'}>
<Button
isSecondary
color={'grey'}
size={'small'}
css={tw`flex-1 mr-4 border-transparent`}
onClick={toggleEditModal}
>
Edit
</Button>
<NewTaskButton schedule={schedule}/>
</Can>
</div>
</div>
<div css={tw`bg-neutral-700 rounded-b`}>
{schedule.tasks.length > 0 ?
schedule.tasks.map(task => (
<ScheduleTaskRow key={`${schedule.id}_${task.id}`} task={task} schedule={schedule}/>
))
:
null
}
</div>
</div>
<EditScheduleModal visible={showEditModal} schedule={schedule} onDismissed={toggleEditModal}/>
<div css={tw`mt-6 flex sm:justify-end`}>
<Can action={'schedule.delete'}> <Can action={'schedule.delete'}>
<DeleteScheduleButton <DeleteScheduleButton
scheduleId={schedule.id} scheduleId={schedule.id}
onDeleted={() => history.push(`/server/${id}/schedules`)} onDeleted={() => history.push(`/server/${id}/schedules`)}
/> />
</Can> </Can>
{schedule.isActive && schedule.tasks.length > 0 &&
<Can action={'schedule.update'}> <Can action={'schedule.update'}>
<Button css={tw`mr-4`} onClick={() => setShowEditModal(true)}> <RunScheduleButton schedule={schedule}/>
Edit
</Button>
<NewTaskButton schedule={schedule}/>
</Can> </Can>
}
</div> </div>
</> </>
} }

View File

@ -4,6 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendarAlt } from '@fortawesome/free-solid-svg-icons'; import { faCalendarAlt } from '@fortawesome/free-solid-svg-icons';
import { format } from 'date-fns'; import { format } from 'date-fns';
import tw from 'twin.macro'; import tw from 'twin.macro';
import ScheduleCronRow from '@/components/server/schedules/ScheduleCronRow';
export default ({ schedule }: { schedule: Schedule }) => ( export default ({ schedule }: { schedule: Schedule }) => (
<> <>
@ -27,36 +28,19 @@ export default ({ schedule }: { schedule: Schedule }) => (
{schedule.isActive ? 'Active' : 'Inactive'} {schedule.isActive ? 'Active' : 'Inactive'}
</p> </p>
</div> </div>
<div css={tw`flex items-center mx-auto sm:mx-8 w-full sm:w-auto mt-4 sm:mt-0`}> <ScheduleCronRow cron={schedule.cron} css={tw`mx-auto sm:mx-8 w-full sm:w-auto mt-4 sm:mt-0`}/>
<div css={tw`w-1/5 sm:w-auto text-center`}>
<p css={tw`font-medium`}>{schedule.cron.minute}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Minute</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{schedule.cron.hour}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Hour</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{schedule.cron.dayOfMonth}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Day (Month)</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>*</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Month</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{schedule.cron.dayOfWeek}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Day (Week)</p>
</div>
</div>
<div> <div>
<p <p
css={[ css={[
tw`py-1 px-3 rounded text-xs uppercase text-white hidden sm:block`, tw`py-1 px-3 rounded text-xs uppercase text-white hidden sm:block`,
schedule.isActive ? tw`bg-green-600` : tw`bg-neutral-400`, schedule.isActive && !schedule.isProcessing ? tw`bg-green-600` : tw`bg-neutral-400`,
]} ]}
> >
{schedule.isActive ? 'Active' : 'Inactive'} {schedule.isProcessing ?
'Processing'
:
schedule.isActive ? 'Active' : 'Inactive'
}
</p> </p>
</div> </div>
</> </>

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules'; import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCode, faFileArchive, faPencilAlt, faToggleOn, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { faClock, faCode, faFileArchive, faPencilAlt, faToggleOn, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask'; import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
@ -11,6 +11,7 @@ import useFlash from '@/plugins/useFlash';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import tw from 'twin.macro'; import tw from 'twin.macro';
import ConfirmationModal from '@/components/elements/ConfirmationModal'; import ConfirmationModal from '@/components/elements/ConfirmationModal';
import Icon from '@/components/elements/Icon';
interface Props { interface Props {
schedule: Schedule; schedule: Schedule;
@ -56,7 +57,7 @@ export default ({ schedule, task }: Props) => {
const [ title, icon ] = getActionDetails(task.action); const [ title, icon ] = getActionDetails(task.action);
return ( return (
<div css={tw`flex flex-wrap items-center bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded`}> <div css={tw`sm:flex items-center p-3 sm:p-6 border-b border-neutral-800`}>
<SpinnerOverlay visible={isLoading} fixed size={'large'}/> <SpinnerOverlay visible={isLoading} fixed size={'large'}/>
{isEditing && <TaskDetailsModal {isEditing && <TaskDetailsModal
schedule={schedule} schedule={schedule}
@ -73,8 +74,8 @@ export default ({ schedule, task }: Props) => {
Are you sure you want to delete this task? This action cannot be undone. Are you sure you want to delete this task? This action cannot be undone.
</ConfirmationModal> </ConfirmationModal>
<FontAwesomeIcon icon={icon} css={tw`text-lg text-white hidden md:block`}/> <FontAwesomeIcon icon={icon} css={tw`text-lg text-white hidden md:block`}/>
<div css={tw`flex-none sm:flex-1 mb-4 sm:mb-0 w-full md:w-auto overflow-x-auto`}> <div css={tw`flex-none sm:flex-1 w-full sm:w-auto overflow-x-auto`}>
<p css={tw`md:ml-6 text-neutral-300 uppercase text-xs`}> <p css={tw`md:ml-6 text-neutral-200 uppercase text-sm`}>
{title} {title}
</p> </p>
{task.payload && {task.payload &&
@ -87,14 +88,13 @@ export default ({ schedule, task }: Props) => {
</div> </div>
} }
</div> </div>
{task.sequenceId > 1 && <div css={tw`mt-3 sm:mt-0 flex items-center w-full sm:w-auto`}>
{task.sequenceId > 1 && task.timeOffset > 0 &&
<div css={tw`mr-6`}> <div css={tw`mr-6`}>
<p css={tw`text-center mb-1`}> <div css={tw`flex items-center px-2 py-1 bg-neutral-500 text-sm rounded-full`}>
{task.timeOffset}s <Icon icon={faClock} css={tw`w-3 h-3 mr-2`}/>
</p> {task.timeOffset}s later
<p css={tw`text-neutral-300 uppercase text-2xs`}> </div>
Delay Run By
</p>
</div> </div>
} }
<Can action={'schedule.update'}> <Can action={'schedule.update'}>
@ -118,5 +118,6 @@ export default ({ schedule, task }: Props) => {
</button> </button>
</Can> </Can>
</div> </div>
</div>
); );
}; };

View File

@ -10,7 +10,7 @@ export default () => {
return ( return (
<> <>
{visible && <EditSubuserModal appear visible onDismissed={() => setVisible(false)}/>} <EditSubuserModal visible={visible} onModalDismissed={() => setVisible(false)}/>
<Button onClick={() => setVisible(true)}> <Button onClick={() => setVisible(true)}>
<FontAwesomeIcon icon={faUserPlus} css={tw`mr-1`}/> New User <FontAwesomeIcon icon={faUserPlus} css={tw`mr-1`}/> New User
</Button> </Button>

View File

@ -1,14 +1,10 @@
import React, { forwardRef, memo, useCallback, useEffect, useRef } from 'react'; import React, { useContext, useEffect, useRef } from 'react';
import { Subuser } from '@/state/server/subusers'; import { Subuser } from '@/state/server/subusers';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import { Form, Formik } from 'formik';
import { array, object, string } from 'yup'; import { array, object, string } from 'yup';
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import { Actions, useStoreActions, useStoreState } from 'easy-peasy'; import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import TitledGreyBox from '@/components/elements/TitledGreyBox';
import Checkbox from '@/components/elements/Checkbox';
import styled from 'styled-components/macro';
import createOrUpdateSubuser from '@/api/server/users/createOrUpdateSubuser'; import createOrUpdateSubuser from '@/api/server/users/createOrUpdateSubuser';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
@ -17,104 +13,33 @@ import { usePermissions } from '@/plugins/usePermissions';
import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo'; import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import Label from '@/components/elements/Label'; import PermissionTitleBox from '@/components/server/users/PermissionTitleBox';
import Input from '@/components/elements/Input'; import asModal from '@/hoc/asModal';
import isEqual from 'react-fast-compare'; import PermissionRow from '@/components/server/users/PermissionRow';
import ModalContext from '@/context/ModalContext';
type Props = { type Props = {
subuser?: Subuser; subuser?: Subuser;
} & RequiredModalProps; };
interface Values { interface Values {
email: string; email: string;
permissions: string[]; permissions: string[];
} }
const PermissionLabel = styled.label` const EditSubuserModal = ({ subuser }: Props) => {
${tw`flex items-center border border-transparent rounded md:p-2 transition-colors duration-75`}; const ref = useRef<HTMLHeadingElement>(null);
text-transform: none; const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const appendSubuser = ServerContext.useStoreActions(actions => actions.subusers.appendSubuser);
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const { dismiss, toggleSpinner } = useContext(ModalContext);
&:not(.disabled) { const isRootAdmin = useStoreState(state => state.user.data!.rootAdmin);
${tw`cursor-pointer`};
&:hover {
${tw`border-neutral-500 bg-neutral-800`};
}
}
&:not(:first-of-type) {
${tw`mt-4 sm:mt-2`};
}
&.disabled {
${tw`opacity-50`};
& input[type="checkbox"]:not(:checked) {
${tw`border-0`};
}
}
`;
interface TitleProps {
isEditable: boolean;
permission: string;
permissions: string[];
children: React.ReactNode;
className?: string;
}
const PermissionTitledBox = memo(({ isEditable, permission, permissions, className, children }: TitleProps) => {
const { values, setFieldValue } = useFormikContext<Values>();
const onCheckboxClicked = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.currentTarget.checked, [
...values.permissions,
...permissions.filter(p => !values.permissions.includes(p)),
]);
if (e.currentTarget.checked) {
setFieldValue('permissions', [
...values.permissions,
...permissions.filter(p => !values.permissions.includes(p)),
]);
} else {
setFieldValue('permissions', [
...values.permissions.filter(p => !permissions.includes(p)),
]);
}
}, [ permissions, values.permissions ]);
return (
<TitledGreyBox
title={
<div css={tw`flex items-center`}>
<p css={tw`text-sm uppercase flex-1`}>{permission}</p>
{isEditable &&
<Input
type={'checkbox'}
checked={permissions.every(p => values.permissions.includes(p))}
onChange={onCheckboxClicked}
/>
}
</div>
}
className={className}
>
{children}
</TitledGreyBox>
);
}, isEqual);
const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
const { isSubmitting } = useFormikContext<Values>();
const [ canEditUser ] = usePermissions(subuser ? [ 'user.update' ] : [ 'user.create' ]);
const permissions = useStoreState(state => state.permissions.data); const permissions = useStoreState(state => state.permissions.data);
const user = useStoreState(state => state.user.data!);
// The currently logged in user's permissions. We're going to filter out any permissions // The currently logged in user's permissions. We're going to filter out any permissions
// that they should not need. // that they should not need.
const loggedInPermissions = ServerContext.useStoreState(state => state.server.permissions); const loggedInPermissions = ServerContext.useStoreState(state => state.server.permissions);
const [ canEditUser ] = usePermissions(subuser ? [ 'user.update' ] : [ 'user.create' ]);
// The permissions that can be modified by this user. // The permissions that can be modified by this user.
const editablePermissions = useDeepCompareMemo(() => { const editablePermissions = useDeepCompareMemo(() => {
@ -123,111 +48,25 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
const list: string[] = ([] as string[]).concat.apply([], Object.values(cleaned)); const list: string[] = ([] as string[]).concat.apply([], Object.values(cleaned));
if (user.rootAdmin || (loggedInPermissions.length === 1 && loggedInPermissions[0] === '*')) { if (isRootAdmin || (loggedInPermissions.length === 1 && loggedInPermissions[0] === '*')) {
return list; return list;
} }
return list.filter(key => loggedInPermissions.indexOf(key) >= 0); return list.filter(key => loggedInPermissions.indexOf(key) >= 0);
}, [ permissions, loggedInPermissions ]); }, [ isRootAdmin, permissions, loggedInPermissions ]);
return ( const submit = (values: Values) => {
<Modal {...props} top={false} showSpinnerOverlay={isSubmitting}> toggleSpinner(true);
<h2 css={tw`text-2xl`} ref={ref}>
{subuser ?
`${canEditUser ? 'Modify' : 'View'} permissions for ${subuser.email}`
:
'Create new subuser'
}
</h2>
<FlashMessageRender byKey={'user:edit'} css={tw`mt-4`}/>
{(!user.rootAdmin && loggedInPermissions[0] !== '*') &&
<div css={tw`mt-4 pl-4 py-2 border-l-4 border-cyan-400`}>
<p css={tw`text-sm text-neutral-300`}>
Only permissions which your account is currently assigned may be selected when creating or
modifying other users.
</p>
</div>
}
{!subuser &&
<div css={tw`mt-6`}>
<Field
name={'email'}
label={'User Email'}
description={'Enter the email address of the user you wish to invite as a subuser for this server.'}
/>
</div>
}
<div css={tw`my-6`}>
{Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => {
const group = Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`);
return (
<PermissionTitledBox
key={`permission_${key}`}
isEditable={canEditUser}
permission={key}
permissions={group}
css={index > 0 ? tw`mt-4` : undefined}
>
<p css={tw`text-sm text-neutral-400 mb-4`}>
{permissions[key].description}
</p>
{Object.keys(permissions[key].keys).map(pkey => (
<PermissionLabel
key={`permission_${key}_${pkey}`}
htmlFor={`permission_${key}_${pkey}`}
className={(!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0) ? 'disabled' : undefined}
>
<div css={tw`p-2`}>
<Checkbox
id={`permission_${key}_${pkey}`}
name={'permissions'}
value={`${key}.${pkey}`}
css={tw`w-5 h-5 mr-2`}
disabled={!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0}
/>
</div>
<div css={tw`flex-1`}>
<Label as={'p'} css={tw`font-medium`}>{pkey}</Label>
{permissions[key].keys[pkey].length > 0 &&
<p css={tw`text-xs text-neutral-400 mt-1`}>
{permissions[key].keys[pkey]}
</p>
}
</div>
</PermissionLabel>
))}
</PermissionTitledBox>
);
})}
</div>
<Can action={subuser ? 'user.update' : 'user.create'}>
<div css={tw`pb-6 flex justify-end`}>
<Button type={'submit'} css={tw`w-full sm:w-auto`}>
{subuser ? 'Save' : 'Invite User'}
</Button>
</div>
</Can>
</Modal>
);
});
export default ({ subuser, ...props }: Props) => {
const ref = useRef<HTMLHeadingElement>(null);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const appendSubuser = ServerContext.useStoreActions(actions => actions.subusers.appendSubuser);
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('user:edit'); clearFlashes('user:edit');
createOrUpdateSubuser(uuid, values, subuser) createOrUpdateSubuser(uuid, values, subuser)
.then(subuser => { .then(subuser => {
appendSubuser(subuser); appendSubuser(subuser);
props.onDismissed(); dismiss();
}) })
.catch(error => { .catch(error => {
console.error(error); console.error(error);
setSubmitting(false); toggleSpinner(false);
clearAndAddHttpError({ key: 'user:edit', error }); clearAndAddHttpError({ key: 'user:edit', error });
if (ref.current) { if (ref.current) {
@ -236,10 +75,8 @@ export default ({ subuser, ...props }: Props) => {
}); });
}; };
useEffect(() => { useEffect(() => () => {
return () => {
clearFlashes('user:edit'); clearFlashes('user:edit');
};
}, []); }, []);
return ( return (
@ -258,8 +95,61 @@ export default ({ subuser, ...props }: Props) => {
})} })}
> >
<Form> <Form>
<EditSubuserModal ref={ref} subuser={subuser} {...props}/> <h2 css={tw`text-2xl`} ref={ref}>
{subuser ? `${canEditUser ? 'Modify' : 'View'} permissions for ${subuser.email}` : 'Create new subuser'}
</h2>
<FlashMessageRender byKey={'user:edit'} css={tw`mt-4`}/>
{(!isRootAdmin && loggedInPermissions[0] !== '*') &&
<div css={tw`mt-4 pl-4 py-2 border-l-4 border-cyan-400`}>
<p css={tw`text-sm text-neutral-300`}>
Only permissions which your account is currently assigned may be selected when creating or
modifying other users.
</p>
</div>
}
{!subuser &&
<div css={tw`mt-6`}>
<Field
name={'email'}
label={'User Email'}
description={'Enter the email address of the user you wish to invite as a subuser for this server.'}
/>
</div>
}
<div css={tw`my-6`}>
{Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => (
<PermissionTitleBox
key={`permission_${key}`}
title={key}
isEditable={canEditUser}
permissions={Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`)}
css={index > 0 ? tw`mt-4` : undefined}
>
<p css={tw`text-sm text-neutral-400 mb-4`}>
{permissions[key].description}
</p>
{Object.keys(permissions[key].keys).map(pkey => (
<PermissionRow
key={`permission_${key}.${pkey}`}
permission={`${key}.${pkey}`}
disabled={!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0}
/>
))}
</PermissionTitleBox>
))}
</div>
<Can action={subuser ? 'user.update' : 'user.create'}>
<div css={tw`pb-6 flex justify-end`}>
<Button type={'submit'} css={tw`w-full sm:w-auto`}>
{subuser ? 'Save' : 'Invite User'}
</Button>
</div>
</Can>
</Form> </Form>
</Formik> </Formik>
); );
}; };
export default asModal<Props>({
top: false,
})(EditSubuserModal);

View File

@ -0,0 +1,65 @@
import styled from 'styled-components/macro';
import tw from 'twin.macro';
import Checkbox from '@/components/elements/Checkbox';
import React from 'react';
import { useStoreState } from 'easy-peasy';
import Label from '@/components/elements/Label';
const Container = styled.label`
${tw`flex items-center border border-transparent rounded md:p-2 transition-colors duration-75`};
text-transform: none;
&:not(.disabled) {
${tw`cursor-pointer`};
&:hover {
${tw`border-neutral-500 bg-neutral-800`};
}
}
&:not(:first-of-type) {
${tw`mt-4 sm:mt-2`};
}
&.disabled {
${tw`opacity-50`};
& input[type="checkbox"]:not(:checked) {
${tw`border-0`};
}
}
`;
interface Props {
permission: string;
disabled: boolean;
}
const PermissionRow = ({ permission, disabled }: Props) => {
const [ key, pkey ] = permission.split('.', 2);
const permissions = useStoreState(state => state.permissions.data);
return (
<Container htmlFor={`permission_${permission}`} className={disabled ? 'disabled' : undefined}>
<div css={tw`p-2`}>
<Checkbox
id={`permission_${permission}`}
name={'permissions'}
value={permission}
css={tw`w-5 h-5 mr-2`}
disabled={disabled}
/>
</div>
<div css={tw`flex-1`}>
<Label as={'p'} css={tw`font-medium`}>{pkey}</Label>
{permissions[key].keys[pkey].length > 0 &&
<p css={tw`text-xs text-neutral-400 mt-1`}>
{permissions[key].keys[pkey]}
</p>
}
</div>
</Container>
);
};
export default PermissionRow;

View File

@ -0,0 +1,50 @@
import React, { memo, useCallback } from 'react';
import { useField } from 'formik';
import TitledGreyBox from '@/components/elements/TitledGreyBox';
import tw from 'twin.macro';
import Input from '@/components/elements/Input';
import isEqual from 'react-fast-compare';
interface Props {
isEditable: boolean;
title: string;
permissions: string[];
className?: string;
}
const PermissionTitleBox: React.FC<Props> = memo(({ isEditable, title, permissions, className, children }) => {
const [ { value }, , { setValue } ] = useField<string[]>('permissions');
const onCheckboxClicked = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.checked) {
setValue([
...value,
...permissions.filter(p => !value.includes(p)),
]);
} else {
setValue(value.filter(p => !permissions.includes(p)));
}
}, [ permissions, value ]);
return (
<TitledGreyBox
title={
<div css={tw`flex items-center`}>
<p css={tw`text-sm uppercase flex-1`}>{title}</p>
{isEditable &&
<Input
type={'checkbox'}
checked={permissions.every(p => value.includes(p))}
onChange={onCheckboxClicked}
/>
}
</div>
}
className={className}
>
{children}
</TitledGreyBox>
);
}, isEqual);
export default PermissionTitleBox;

View File

@ -19,14 +19,11 @@ export default ({ subuser }: Props) => {
return ( return (
<GreyRowBox css={tw`mb-2`}> <GreyRowBox css={tw`mb-2`}>
{visible &&
<EditSubuserModal <EditSubuserModal
appear
visible
subuser={subuser} subuser={subuser}
onDismissed={() => setVisible(false)} visible={visible}
onModalDismissed={() => setVisible(false)}
/> />
}
<div css={tw`w-10 h-10 rounded-full bg-white border-2 border-neutral-800 overflow-hidden hidden md:block`}> <div css={tw`w-10 h-10 rounded-full bg-white border-2 border-neutral-800 overflow-hidden hidden md:block`}>
<img css={tw`w-full h-full`} src={`${subuser.image}?s=400`}/> <img css={tw`w-full h-full`} src={`${subuser.image}?s=400`}/>
</div> </div>

View File

@ -9,7 +9,7 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import getServerSubusers from '@/api/server/users/getServerSubusers'; import getServerSubusers from '@/api/server/users/getServerSubusers';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import PageContentBlock from '@/components/elements/PageContentBlock'; import ServerContentBlock from '@/components/elements/ServerContentBlock';
import tw from 'twin.macro'; import tw from 'twin.macro';
export default () => { export default () => {
@ -48,7 +48,7 @@ export default () => {
} }
return ( return (
<PageContentBlock title={'Subusers'}> <ServerContentBlock title={'Subusers'}>
<FlashMessageRender byKey={'users'} css={tw`mb-4`}/> <FlashMessageRender byKey={'users'} css={tw`mb-4`}/>
{!subusers.length ? {!subusers.length ?
<p css={tw`text-center text-sm text-neutral-400`}> <p css={tw`text-center text-sm text-neutral-400`}>
@ -64,6 +64,6 @@ export default () => {
<AddSubuserButton/> <AddSubuserButton/>
</div> </div>
</Can> </Can>
</PageContentBlock> </ServerContentBlock>
); );
}; };

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import Modal, { ModalProps } from '@/components/elements/Modal'; import Modal, { ModalProps } from '@/components/elements/Modal';
import ModalContext from '@/context/ModalContext'; import ModalContext from '@/context/ModalContext';
import isEqual from 'react-fast-compare';
export interface AsModalProps { export interface AsModalProps {
visible: boolean; visible: boolean;
@ -13,7 +12,7 @@ type ExtendedModalProps = Omit<ModalProps, 'appear' | 'visible' | 'onDismissed'>
interface State { interface State {
render: boolean; render: boolean;
visible: boolean; visible: boolean;
modalProps: ExtendedModalProps | undefined; showSpinnerOverlay?: boolean;
} }
type ExtendedComponentType<T> = (C: React.ComponentType<T>) => React.ComponentType<T & AsModalProps>; type ExtendedComponentType<T> = (C: React.ComponentType<T>) => React.ComponentType<T & AsModalProps>;
@ -30,17 +29,18 @@ function asModal<P extends object> (modalProps?: ExtendedModalProps | ((props: P
this.state = { this.state = {
render: props.visible, render: props.visible,
visible: props.visible, visible: props.visible,
modalProps: typeof modalProps === 'function' ? modalProps(this.props) : modalProps, showSpinnerOverlay: undefined,
};
}
get modalProps () {
return {
...(typeof modalProps === 'function' ? modalProps(this.props) : modalProps),
showSpinnerOverlay: this.state.showSpinnerOverlay,
}; };
} }
componentDidUpdate (prevProps: Readonly<P & AsModalProps>) { componentDidUpdate (prevProps: Readonly<P & AsModalProps>) {
const mapped = typeof modalProps === 'function' ? modalProps(this.props) : modalProps;
if (!isEqual(this.state.modalProps, mapped)) {
// noinspection JSPotentiallyInvalidUsageOfThis
this.setState({ modalProps: mapped });
}
if (prevProps.visible && !this.props.visible) { if (prevProps.visible && !this.props.visible) {
// noinspection JSPotentiallyInvalidUsageOfThis // noinspection JSPotentiallyInvalidUsageOfThis
this.setState({ visible: false }); this.setState({ visible: false });
@ -52,22 +52,10 @@ function asModal<P extends object> (modalProps?: ExtendedModalProps | ((props: P
dismiss = () => this.setState({ visible: false }); dismiss = () => this.setState({ visible: false });
toggleSpinner = (value?: boolean) => this.setState(s => ({ toggleSpinner = (value?: boolean) => this.setState({ showSpinnerOverlay: value });
modalProps: {
...s.modalProps,
showSpinnerOverlay: value || false,
},
}));
render () { render () {
return ( return (
<ModalContext.Provider
value={{
dismiss: this.dismiss.bind(this),
toggleSpinner: this.toggleSpinner.bind(this),
}}
>
{
this.state.render ? this.state.render ?
<Modal <Modal
appear appear
@ -77,14 +65,19 @@ function asModal<P extends object> (modalProps?: ExtendedModalProps | ((props: P
this.props.onModalDismissed(); this.props.onModalDismissed();
} }
})} })}
{...this.state.modalProps} {...this.modalProps}
>
<ModalContext.Provider
value={{
dismiss: this.dismiss.bind(this),
toggleSpinner: this.toggleSpinner.bind(this),
}}
> >
<Component {...this.props}/> <Component {...this.props}/>
</ModalContext.Provider>
</Modal> </Modal>
: :
null null
}
</ModalContext.Provider>
); );
} }
}; };

View File

@ -28,6 +28,7 @@ import NetworkContainer from '@/components/server/network/NetworkContainer';
import InstallListener from '@/components/server/InstallListener'; import InstallListener from '@/components/server/InstallListener';
import StartupContainer from '@/components/server/startup/StartupContainer'; import StartupContainer from '@/components/server/startup/StartupContainer';
import requireServerPermission from '@/hoc/requireServerPermission'; import requireServerPermission from '@/hoc/requireServerPermission';
import ErrorBoundary from '@/components/elements/ErrorBoundary';
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin); const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
@ -120,7 +121,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
message={'Please check back in a few minutes.'} message={'Please check back in a few minutes.'}
/> />
: :
<> <ErrorBoundary>
<TransitionRouter> <TransitionRouter>
<Switch location={location}> <Switch location={location}>
<Route path={`${match.path}`} component={ServerConsole} exact/> <Route path={`${match.path}`} component={ServerConsole} exact/>
@ -173,7 +174,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
<Route path={'*'} component={NotFound}/> <Route path={'*'} component={NotFound}/>
</Switch> </Switch>
</TransitionRouter> </TransitionRouter>
</> </ErrorBoundary>
} }
</> </>
} }

View File

@ -11,7 +11,7 @@
@endsection @endsection
@section('content-header') @section('content-header')
<h1>Mounts<small>SoonTM</small></h1> <h1>Mounts<small>Configure and manage additional mount points for servers.</small></h1>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li> <li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Mounts</li> <li class="active">Mounts</li>

Some files were not shown because too many files have changed in this diff Show More