Merge branch 'develop' into develop
This commit is contained in:
commit
665a4dd8a4
115 changed files with 3434 additions and 1970 deletions
2
.eslintignore
Normal file
2
.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
public
|
||||
resources/views
|
14
.github/ISSUE_TEMPLATE/--installation-help.md
vendored
14
.github/ISSUE_TEMPLATE/--installation-help.md
vendored
|
@ -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
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -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.
|
||||
|
||||
## 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
|
||||
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. 🎉
|
||||
|
|
10
README.md
10
README.md
|
@ -3,7 +3,6 @@
|
|||
![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)
|
||||
![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)
|
||||
|
||||
# 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)
|
||||
|
||||
## 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)
|
||||
|
||||
| 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. |
|
||||
| [**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. |
|
||||
| [**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! |
|
||||
| [**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 RoyaleHosting’s reliable servers and network. Easy to use, provisioned in a couple of minutes. |
|
||||
| [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHosting’s 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
|
||||
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
||||
|
|
|
@ -144,6 +144,11 @@ class AppSettingsCommand extends Command
|
|||
$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->writeToEnvironment($this->variables);
|
||||
|
||||
|
|
|
@ -1,62 +1,37 @@
|
|||
<?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;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Throwable;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Pterodactyl\Services\Schedules\ProcessScheduleService;
|
||||
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
|
||||
|
||||
class ProcessRunnableCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Process schedules in the database and determine which are ready to run.';
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
|
||||
*/
|
||||
protected $processScheduleService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
|
||||
*/
|
||||
protected $repository;
|
||||
protected $signature = 'p:schedule:process';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'p:schedule:process';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
protected $description = 'Process schedules in the database and determine which are ready to run.';
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
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) {
|
||||
$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));
|
||||
$schedules->each(function ($schedule) use ($bar) {
|
||||
if ($schedule->tasks instanceof Collection && count($schedule->tasks) > 0) {
|
||||
$this->processScheduleService->handle($schedule);
|
||||
|
||||
if ($this->input->isInteractive()) {
|
||||
$bar->clear();
|
||||
$this->line(trans('command/messages.schedule.output_line', [
|
||||
'schedule' => $schedule->name,
|
||||
'hash' => $schedule->hashid,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($schedules as $schedule) {
|
||||
$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', [
|
||||
'schedule' => $schedule->name,
|
||||
'hash' => $schedule->hashid,
|
||||
]));
|
||||
} catch (Throwable | Exception $exception) {
|
||||
Log::error($exception, ['schedule_id' => $schedule->id]);
|
||||
|
||||
$this->error("An error was encountered while processing Schedule #{$schedule->id}: " . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,8 +58,9 @@ class DeleteUserCommand extends Command
|
|||
Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
|
||||
|
||||
$results = User::query()
|
||||
->where('email', 'LIKE', "$search%")
|
||||
->where('username', 'LIKE', "$search%")
|
||||
->where('id', 'LIKE', "$search%")
|
||||
->orWhere('username', 'LIKE', "$search%")
|
||||
->orWhere('email', 'LIKE', "$search%")
|
||||
->get();
|
||||
|
||||
if (count($results) < 1) {
|
||||
|
|
|
@ -15,16 +15,6 @@ interface ScheduleRepositoryInterface extends RepositoryInterface
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -34,12 +24,4 @@ interface ScheduleRepositoryInterface extends RepositoryInterface
|
|||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -139,16 +139,4 @@ interface ServerRepositoryInterface extends RepositoryInterface
|
|||
* @return \Illuminate\Contracts\Pagination\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
25
app/Helpers/Time.php
Normal 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));
|
||||
}
|
||||
}
|
|
@ -52,7 +52,12 @@ class Utilities
|
|||
)->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');
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Pterodactyl\Http\Controllers\Admin;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Nest;
|
||||
use Pterodactyl\Models\Mount;
|
||||
|
@ -101,7 +102,6 @@ class MountController extends Controller
|
|||
*/
|
||||
public function create(MountFormRequest $request)
|
||||
{
|
||||
/** @var \Pterodactyl\Models\Mount $mount */
|
||||
$model = (new Mount())->fill($request->validated());
|
||||
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@ use Illuminate\Http\Request;
|
|||
use Pterodactyl\Models\Server;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Models\Filters\AdminServerFilter;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
|
||||
class ServerController extends Controller
|
||||
|
@ -45,8 +47,10 @@ class ServerController extends Controller
|
|||
public function index(Request $request)
|
||||
{
|
||||
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
|
||||
->allowedFilters(['uuid', 'name', 'image'])
|
||||
->allowedSorts(['id', 'uuid'])
|
||||
->allowedFilters([
|
||||
AllowedFilter::exact('owner_id'),
|
||||
AllowedFilter::custom('*', new AdminServerFilter),
|
||||
])
|
||||
->paginate(config()->get('pterodactyl.paginate.admin.servers'));
|
||||
|
||||
return $this->view->make('admin.servers.index', ['servers' => $servers]);
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace Pterodactyl\Http\Controllers\Api\Client;
|
|||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Pterodactyl\Models\Filters\MultiFieldServerFilter;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\ServerTransformer;
|
||||
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.
|
||||
$builder = QueryBuilder::for(
|
||||
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
|
||||
// just return all of the servers the user has access to because they are the owner or a subuser of the
|
||||
// server.
|
||||
if ($request->input('type') === 'admin') {
|
||||
$builder = $user->root_admin
|
||||
? $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
|
||||
// make it a query that will never return any results back.
|
||||
: $builder->whereRaw('1 = 2');
|
||||
} elseif ($request->input('type') === 'owner') {
|
||||
$builder = $builder->where('owner_id', $user->id);
|
||||
// server. If ?type=admin-all is passed all servers on the system will be returned to the user, rather
|
||||
// than only servers they can see because they are an admin.
|
||||
if (in_array($type, ['admin', 'admin-all'])) {
|
||||
// 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.
|
||||
if (! $user->root_admin) {
|
||||
$builder->whereRaw('1 = 2');
|
||||
} else {
|
||||
$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('id', $user->accessibleServers()->pluck('id')->all());
|
||||
$builder = $builder->whereIn('servers.id', $user->accessibleServers()->pluck('id')->all());
|
||||
}
|
||||
|
||||
$servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query());
|
||||
|
|
|
@ -10,15 +10,19 @@ use Pterodactyl\Models\Server;
|
|||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Helpers\Utilities;
|
||||
use Pterodactyl\Jobs\Schedule\RunTaskJob;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
|
||||
use Pterodactyl\Services\Schedules\ProcessScheduleService;
|
||||
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
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\StoreScheduleRequest;
|
||||
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\TriggerScheduleRequest;
|
||||
|
||||
class ScheduleController extends ClientApiController
|
||||
{
|
||||
|
@ -27,16 +31,23 @@ class ScheduleController extends ClientApiController
|
|||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* ScheduleController constructor.
|
||||
*
|
||||
* @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();
|
||||
|
||||
$this->repository = $repository;
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,6 +158,30 @@ class ScheduleController extends ClientApiController
|
|||
->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.
|
||||
*
|
||||
|
|
|
@ -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 */
|
||||
$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\Models\Server $server
|
||||
* @param \Pterodactyl\Models\Schedule $schedule
|
||||
* @param \Pterodactyl\Models\Task $task
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
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.');
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Repositories\Eloquent\NodeRepository;
|
||||
use Pterodactyl\Services\Eggs\EggConfigurationService;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Http\Resources\Wings\ServerConfigurationCollection;
|
||||
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
|
||||
|
||||
class ServerDetailsController extends Controller
|
||||
|
@ -27,11 +30,6 @@ class ServerDetailsController extends Controller
|
|||
*/
|
||||
private $configurationStructureService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
|
||||
*/
|
||||
private $nodeRepository;
|
||||
|
||||
/**
|
||||
* ServerConfigurationController constructor.
|
||||
*
|
||||
|
@ -49,7 +47,6 @@ class ServerDetailsController extends Controller
|
|||
$this->eggConfigurationService = $eggConfigurationService;
|
||||
$this->repository = $repository;
|
||||
$this->configurationStructureService = $configurationStructureService;
|
||||
$this->nodeRepository = $nodeRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,7 +63,7 @@ class ServerDetailsController extends Controller
|
|||
{
|
||||
$server = $this->repository->getByUuid($uuid);
|
||||
|
||||
return JsonResponse::create([
|
||||
return new JsonResponse([
|
||||
'settings' => $this->configurationStructureService->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.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @return \Pterodactyl\Http\Resources\Wings\ServerConfigurationCollection
|
||||
*/
|
||||
public function list(Request $request)
|
||||
{
|
||||
/** @var \Pterodactyl\Models\Node $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) {
|
||||
$configurations[$server->uuid] = [
|
||||
'settings' => $this->configurationStructureService->handle($server),
|
||||
'process_configuration' => $this->eggConfigurationService->handle($server),
|
||||
];
|
||||
}
|
||||
|
||||
return JsonResponse::create($configurations);
|
||||
return new ServerConfigurationCollection($servers);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
namespace Pterodactyl\Http\Controllers\Auth;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Auth\AuthManager;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest;
|
||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
|
||||
|
@ -80,29 +82,31 @@ class LoginCheckpointController extends AbstractLoginController
|
|||
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
|
||||
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
|
||||
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Exception
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function __invoke(LoginCheckpointRequest $request): JsonResponse
|
||||
{
|
||||
$token = $request->input('confirmation_token');
|
||||
$recoveryToken = $request->input('recovery_token');
|
||||
|
||||
try {
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = $this->repository->find($this->cache->get($token, 0));
|
||||
} catch (RecordNotFoundException $exception) {
|
||||
return $this->sendFailedLoginResponse($request, null, 'The authentication token provided has expired, please refresh the page and try again.');
|
||||
if ($this->hasTooManyLoginAttempts($request)) {
|
||||
$this->sendLockoutResponse($request);
|
||||
}
|
||||
|
||||
// If we got a recovery token try to find one that matches for the user and then continue
|
||||
// through the process (and delete the token).
|
||||
if (! is_null($recoveryToken)) {
|
||||
foreach ($user->recoveryTokens as $token) {
|
||||
if (password_verify($recoveryToken, $token->token)) {
|
||||
$this->recoveryTokenRepository->delete($token->id);
|
||||
$token = $request->input('confirmation_token');
|
||||
try {
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::query()->findOrFail($this->cache->get($token, 0));
|
||||
} catch (ModelNotFoundException $exception) {
|
||||
$this->incrementLoginAttempts($request);
|
||||
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
}
|
||||
return $this->sendFailedLoginResponse(
|
||||
$request, null, 'The authentication token provided has expired, please refresh the page and try again.'
|
||||
);
|
||||
}
|
||||
|
||||
// Recovery tokens go through a slightly different pathway for usage.
|
||||
if (! is_null($recoveryToken = $request->input('recovery_token'))) {
|
||||
if ($this->isValidRecoveryToken($user, $recoveryToken)) {
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
}
|
||||
} else {
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ class DaemonAuthenticate
|
|||
// Ensure that all of the correct parts are provided in the header.
|
||||
if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) {
|
||||
throw new BadRequestHttpException(
|
||||
'The Authorization headed provided was not in a valid format.'
|
||||
'The Authorization header provided was not in a valid format.'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
|||
'feature_limits' => 'required|array',
|
||||
'feature_limits.databases' => $rules['database_limit'],
|
||||
'feature_limits.allocations' => $rules['allocation_limit'],
|
||||
'feature_limits.backups' => $rules['backup_limit'],
|
||||
|
||||
// Placeholders for rules added in withValidator() function.
|
||||
'allocation.default' => '',
|
||||
|
@ -102,6 +103,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
|||
'start_on_completion' => array_get($data, 'start_on_completion', false),
|
||||
'database_limit' => array_get($data, 'feature_limits.databases'),
|
||||
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
|
||||
'backup_limit' => array_get($data, 'feature_limits.backups'),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
|||
'feature_limits' => 'required|array',
|
||||
'feature_limits.databases' => $rules['database_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['allocation_id'] = $data['allocation'];
|
||||
$data['database_limit'] = $data['feature_limits']['databases'];
|
||||
$data['allocation_limit'] = $data['feature_limits']['allocations'];
|
||||
$data['database_limit'] = $data['feature_limits']['databases'] ?? null;
|
||||
$data['allocation_limit'] = $data['feature_limits']['allocations'] ?? null;
|
||||
$data['backup_limit'] = $data['feature_limits']['backups'] ?? null;
|
||||
unset($data['allocation'], $data['feature_limits']);
|
||||
|
||||
// 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',
|
||||
'feature_limits.databases' => 'Database Limit',
|
||||
'feature_limits.allocations' => 'Allocation Limit',
|
||||
'feature_limits.backups' => 'Backup Limit',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
}
|
35
app/Http/Resources/Wings/ServerConfigurationCollection.php
Normal file
35
app/Http/Resources/Wings/ServerConfigurationCollection.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -7,11 +7,12 @@ use Pterodactyl\Jobs\Job;
|
|||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\Task;
|
||||
use InvalidArgumentException;
|
||||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Pterodactyl\Repositories\Eloquent\TaskRepository;
|
||||
use Pterodactyl\Services\Backups\InitiateBackupService;
|
||||
use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
|
||||
|
@ -42,15 +43,13 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
* @param \Pterodactyl\Repositories\Wings\DaemonCommandRepository $commandRepository
|
||||
* @param \Pterodactyl\Services\Backups\InitiateBackupService $backupService
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(
|
||||
DaemonCommandRepository $commandRepository,
|
||||
InitiateBackupService $backupService,
|
||||
DaemonPowerRepository $powerRepository,
|
||||
TaskRepository $taskRepository
|
||||
DaemonPowerRepository $powerRepository
|
||||
) {
|
||||
// Do not process a task that is not set to active.
|
||||
if (! $this->task->schedule->is_active) {
|
||||
|
|
40
app/Models/Filters/AdminServerFilter.php
Normal file
40
app/Models/Filters/AdminServerFilter.php
Normal 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');
|
||||
}
|
||||
}
|
74
app/Models/Filters/MultiFieldServerFilter.php
Normal file
74
app/Models/Filters/MultiFieldServerFilter.php
Normal 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%"]);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Illuminate\Validation\Rules\NotIn;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $uuid
|
||||
|
@ -63,6 +65,20 @@ class Mount extends Model
|
|||
'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.
|
||||
*
|
||||
|
@ -70,6 +86,26 @@ class Mount extends Model
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Cron\CronExpression;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Container\Container;
|
||||
use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
||||
|
||||
|
@ -114,6 +116,20 @@ class Schedule extends Model
|
|||
'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.
|
||||
*
|
||||
|
|
|
@ -83,6 +83,13 @@ class Server extends Model
|
|||
'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.
|
||||
*
|
||||
|
@ -122,7 +129,7 @@ class Server extends Model
|
|||
'installed' => 'in:0,1,2',
|
||||
'database_limit' => 'present|nullable|integer|min:0',
|
||||
'allocation_limit' => 'sometimes|nullable|integer|min:0',
|
||||
'backup_limit' => 'present|integer|min:0',
|
||||
'backup_limit' => 'present|nullable|integer|min:0',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,23 +31,6 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor
|
|||
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.
|
||||
*
|
||||
|
@ -64,19 +47,4 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -270,22 +270,4 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
|
|||
->where('node_id', '=', $node)
|
||||
->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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace Pterodactyl\Services\Deployment;
|
|||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
|
||||
|
||||
class FindViableNodesService
|
||||
|
@ -32,7 +31,7 @@ class FindViableNodesService
|
|||
*/
|
||||
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;
|
||||
|
||||
|
@ -97,8 +96,8 @@ class FindViableNodesService
|
|||
}
|
||||
|
||||
$results = $query->groupBy('nodes.id')
|
||||
->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.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])
|
||||
->get()
|
||||
->toBase();
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
namespace Pterodactyl\Services\Schedules;
|
||||
|
||||
use Cron\CronExpression;
|
||||
use Exception;
|
||||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Pterodactyl\Jobs\Schedule\RunTaskJob;
|
||||
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
|
||||
class ProcessScheduleService
|
||||
{
|
||||
|
@ -17,63 +17,66 @@ class ProcessScheduleService
|
|||
private $dispatcher;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
|
||||
* @var \Illuminate\Database\ConnectionInterface
|
||||
*/
|
||||
private $scheduleRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface
|
||||
*/
|
||||
private $taskRepository;
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* ProcessScheduleService constructor.
|
||||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Illuminate\Contracts\Bus\Dispatcher $dispatcher
|
||||
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $scheduleRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $taskRepository
|
||||
*/
|
||||
public function __construct(
|
||||
Dispatcher $dispatcher,
|
||||
ScheduleRepositoryInterface $scheduleRepository,
|
||||
TaskRepositoryInterface $taskRepository
|
||||
) {
|
||||
public function __construct(ConnectionInterface $connection, Dispatcher $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->scheduleRepository = $scheduleRepository;
|
||||
$this->taskRepository = $taskRepository;
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a schedule and push the first task onto the queue worker.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Schedule $schedule
|
||||
* @param bool $now
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(Schedule $schedule)
|
||||
public function handle(Schedule $schedule, bool $now = false)
|
||||
{
|
||||
$this->scheduleRepository->loadTasks($schedule);
|
||||
|
||||
/** @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',
|
||||
$schedule->cron_minute,
|
||||
$schedule->cron_hour,
|
||||
$schedule->cron_day_of_month,
|
||||
$schedule->cron_day_of_week
|
||||
);
|
||||
if (is_null($task)) {
|
||||
throw new DisplayException(
|
||||
'Cannot process schedule for task execution: no tasks are registered.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->scheduleRepository->update($schedule->id, [
|
||||
'is_processing' => true,
|
||||
'next_run_at' => CronExpression::factory($formattedCron)->getNextRunDate(),
|
||||
]);
|
||||
$this->connection->transaction(function () use ($schedule, $task) {
|
||||
$schedule->forceFill([
|
||||
'is_processing' => true,
|
||||
'next_run_at' => $schedule->getNextRunDate(),
|
||||
])->saveOrFail();
|
||||
|
||||
$this->taskRepository->update($task->id, ['is_queued' => true]);
|
||||
$task->update(['is_queued' => true]);
|
||||
});
|
||||
|
||||
$this->dispatcher->dispatch(
|
||||
(new RunTaskJob($task))->delay($task->time_offset)
|
||||
);
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,22 +4,14 @@ namespace Pterodactyl\Services\Servers;
|
|||
|
||||
use Illuminate\Support\Arr;
|
||||
use Pterodactyl\Models\Server;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
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
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
|
||||
*/
|
||||
private $allocationRepository;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Database\ConnectionInterface
|
||||
*/
|
||||
|
@ -30,11 +22,6 @@ class BuildModificationService
|
|||
*/
|
||||
private $daemonServerRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
|
||||
*/
|
||||
|
@ -43,23 +30,17 @@ class BuildModificationService
|
|||
/**
|
||||
* BuildModificationService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
|
||||
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(
|
||||
AllocationRepositoryInterface $allocationRepository,
|
||||
ServerConfigurationStructureService $structureService,
|
||||
ConnectionInterface $connection,
|
||||
DaemonServerRepository $daemonServerRepository,
|
||||
ServerRepositoryInterface $repository
|
||||
DaemonServerRepository $daemonServerRepository
|
||||
) {
|
||||
$this->allocationRepository = $allocationRepository;
|
||||
$this->daemonServerRepository = $daemonServerRepository;
|
||||
$this->connection = $connection;
|
||||
$this->repository = $repository;
|
||||
$this->structureService = $structureService;
|
||||
}
|
||||
|
||||
|
@ -70,9 +51,8 @@ class BuildModificationService
|
|||
* @param array $data
|
||||
* @return \Pterodactyl\Models\Server
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function handle(Server $server, array $data)
|
||||
{
|
||||
|
@ -82,48 +62,35 @@ class BuildModificationService
|
|||
|
||||
if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) {
|
||||
try {
|
||||
$this->allocationRepository->findFirstWhere([
|
||||
['id', '=', $data['allocation_id']],
|
||||
['server_id', '=', $server->id],
|
||||
]);
|
||||
} catch (RecordNotFoundException $ex) {
|
||||
throw new DisplayException(trans('admin/server.exceptions.default_allocation_not_found'));
|
||||
Allocation::query()->where('id', $data['allocation_id'])->where('server_id', $server->id)->firstOrFail();
|
||||
} catch (ModelNotFoundException $ex) {
|
||||
throw new DisplayException('The requested default allocation is not currently assigned to this server.');
|
||||
}
|
||||
}
|
||||
|
||||
/* @var \Pterodactyl\Models\Server $server */
|
||||
$server = $this->repository->withFreshModel()->update($server->id, [
|
||||
'oom_disabled' => array_get($data, 'oom_disabled'),
|
||||
'memory' => array_get($data, 'memory'),
|
||||
'swap' => array_get($data, 'swap'),
|
||||
'io' => array_get($data, 'io'),
|
||||
'cpu' => array_get($data, 'cpu'),
|
||||
'threads' => array_get($data, 'threads'),
|
||||
'disk' => array_get($data, 'disk'),
|
||||
'allocation_id' => array_get($data, 'allocation_id'),
|
||||
'database_limit' => array_get($data, 'database_limit', 0),
|
||||
'allocation_limit' => array_get($data, 'allocation_limit', 0),
|
||||
'backup_limit' => array_get($data, 'backup_limit', 0),
|
||||
]);
|
||||
// If any of these values are passed through in the data array go ahead and set
|
||||
// them correctly on the server model.
|
||||
$merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
|
||||
|
||||
$server->forceFill(array_merge($merge, [
|
||||
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
|
||||
'allocation_limit' => Arr::get($data, 'allocation_limit', 0) ?? null,
|
||||
'backup_limit' => Arr::get($data, 'backup_limit', 0) ?? 0,
|
||||
]))->saveOrFail();
|
||||
|
||||
$server = $server->fresh();
|
||||
|
||||
$updateData = $this->structureService->handle($server);
|
||||
|
||||
try {
|
||||
$this->daemonServerRepository
|
||||
->setServer($server)
|
||||
->update(Arr::only($updateData, ['build']));
|
||||
$this->daemonServerRepository->setServer($server)->update($updateData['build'] ?? []);
|
||||
|
||||
$this->connection->commit();
|
||||
} catch (RequestException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
$this->connection->commit();
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the allocations being assigned in the data and ensure they
|
||||
* are available for a server.
|
||||
* Process the allocations being assigned in the data and ensure they are available for a server.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param array $data
|
||||
|
@ -132,55 +99,53 @@ class BuildModificationService
|
|||
*/
|
||||
private function processAllocations(Server $server, array &$data)
|
||||
{
|
||||
$firstAllocationId = null;
|
||||
|
||||
if (! array_key_exists('add_allocations', $data) && ! array_key_exists('remove_allocations', $data)) {
|
||||
if (empty($data['add_allocations']) && empty($data['remove_allocations'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the addition of allocations to this server.
|
||||
if (array_key_exists('add_allocations', $data) && ! empty($data['add_allocations'])) {
|
||||
$unassigned = $this->allocationRepository->getUnassignedAllocationIds($server->node_id);
|
||||
// Handle the addition of allocations to this server. Only assign allocations that are not currently
|
||||
// assigned to a different server, and only allocations on the same node as the server.
|
||||
if (! empty($data['add_allocations'])) {
|
||||
$query = Allocation::query()
|
||||
->where('node_id', $server->node_id)
|
||||
->whereIn('id', $data['add_allocations'])
|
||||
->whereNull('server_id');
|
||||
|
||||
$updateIds = [];
|
||||
foreach ($data['add_allocations'] as $allocation) {
|
||||
if (! in_array($allocation, $unassigned)) {
|
||||
continue;
|
||||
}
|
||||
// Keep track of all the allocations we're just now adding so that we can use the first
|
||||
// one to reset the default allocation to.
|
||||
$freshlyAllocated = $query->pluck('id')->first();
|
||||
|
||||
$firstAllocationId = $firstAllocationId ?? $allocation;
|
||||
$updateIds[] = $allocation;
|
||||
}
|
||||
|
||||
if (! empty($updateIds)) {
|
||||
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $server->id]);
|
||||
}
|
||||
$query->update(['server_id' => $server->id, 'notes' => null]);
|
||||
}
|
||||
|
||||
// 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 = [];
|
||||
if (! empty($data['remove_allocations'])) {
|
||||
foreach ($data['remove_allocations'] as $allocation) {
|
||||
if (! in_array($allocation, $assigned)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($allocation == $data['allocation_id']) {
|
||||
if (is_null($firstAllocationId)) {
|
||||
throw new DisplayException(trans('admin/server.exceptions.no_new_default_allocation'));
|
||||
// If we are attempting to remove the default allocation for the server, see if we can reassign
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
$data['allocation_id'] = $firstAllocationId;
|
||||
// Update the default allocation to be the first allocation that we are creating.
|
||||
$data['allocation_id'] = $freshlyAllocated;
|
||||
}
|
||||
|
||||
$updateIds[] = $allocation;
|
||||
}
|
||||
|
||||
if (! empty($updateIds)) {
|
||||
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => null]);
|
||||
}
|
||||
// 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.
|
||||
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...
|
||||
->whereIn('id', array_diff($data['remove_allocations'], $data['add_allocations'] ?? []))
|
||||
->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ use Pterodactyl\Models\Server;
|
|||
|
||||
class ServerConfigurationStructureService
|
||||
{
|
||||
const REQUIRED_RELATIONS = ['allocation', 'allocations', 'egg'];
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\EnvironmentService
|
||||
*/
|
||||
|
@ -31,13 +29,11 @@ class ServerConfigurationStructureService
|
|||
* daemon, if you modify the structure eggs will break unexpectedly.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param bool $legacy
|
||||
* @param bool $legacy deprecated
|
||||
* @return array
|
||||
*/
|
||||
public function handle(Server $server, bool $legacy = false): array
|
||||
{
|
||||
$server->loadMissing(self::REQUIRED_RELATIONS);
|
||||
|
||||
return $legacy ?
|
||||
$this->returnLegacyFormat($server)
|
||||
: $this->returnCurrentFormat($server);
|
||||
|
@ -93,6 +89,7 @@ class ServerConfigurationStructureService
|
|||
*
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return array
|
||||
* @deprecated
|
||||
*/
|
||||
protected function returnLegacyFormat(Server $server)
|
||||
{
|
||||
|
|
|
@ -33,6 +33,8 @@ return [
|
|||
// backup for that server lives within that folder.
|
||||
'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),
|
||||
],
|
||||
],
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Pterodactyl\Helpers\Time;
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -43,6 +45,7 @@ return [
|
|||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => env('DB_PREFIX', ''),
|
||||
'strict' => env('DB_STRICT_MODE', false),
|
||||
'timezone' => env('DB_TIMEZONE', Time::getMySQLTimezoneOffset(env('APP_TIMEZONE')))
|
||||
],
|
||||
|
||||
/*
|
||||
|
@ -65,6 +68,7 @@ return [
|
|||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'strict' => false,
|
||||
'timezone' => env('DB_TIMEZONE', Time::getMySQLTimezoneOffset(env('APP_TIMEZONE')))
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
@ -58,6 +58,8 @@ return [
|
|||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -188,5 +188,5 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'same_site' => null,
|
||||
'same_site' => env('SESSION_SAMESITE_COOKIE', 'lax'),
|
||||
];
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2020-04-12T16:00:51-07:00",
|
||||
"exported_at": "2020-10-19T23:22:26+00:00",
|
||||
"name": "Bungeecord",
|
||||
"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.",
|
||||
|
@ -17,9 +17,9 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "alpine:3.9",
|
||||
"entrypoint": "ash"
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The version of Bungeecord to download and use.",
|
||||
"env_variable": "BUNGEE_VERSION",
|
||||
"default_value": "latest",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|alpha_num|between:1,6"
|
||||
},
|
||||
{
|
||||
|
@ -37,8 +37,8 @@
|
|||
"description": "The name of the Jarfile to use when running Bungeecord.",
|
||||
"env_variable": "SERVER_JARFILE",
|
||||
"default_value": "bungeecord.jar",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2020-05-24T12:15:13-04:00",
|
||||
"exported_at": "2020-10-19T23:22:28+00:00",
|
||||
"name": "Forge Minecraft",
|
||||
"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.",
|
||||
|
@ -17,7 +17,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The name of the Jarfile to use when running Forge Mod.",
|
||||
"env_variable": "SERVER_JARFILE",
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"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.",
|
||||
"env_variable": "MC_VERSION",
|
||||
"default_value": "latest",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"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\".",
|
||||
"env_variable": "BUILD_TYPE",
|
||||
"default_value": "recommended",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"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.",
|
||||
"env_variable": "FORGE_VERSION",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:20"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2019-08-01T04:49:37-04:00",
|
||||
"exported_at": "2020-10-25T21:03:55+00:00",
|
||||
"name": "Paper",
|
||||
"author": "parker@pterodactyl.io",
|
||||
"description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.",
|
||||
|
@ -17,9 +17,9 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "alpine:3.9",
|
||||
"entrypoint": "ash"
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"env_variable": "MINECRAFT_VERSION",
|
||||
"default_value": "latest",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "nullable|string|max:20"
|
||||
},
|
||||
{
|
||||
|
@ -37,8 +37,8 @@
|
|||
"description": "The name of the server jarfile to run the server with.",
|
||||
"env_variable": "SERVER_JARFILE",
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"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.",
|
||||
"env_variable": "DL_PATH",
|
||||
"default_value": "",
|
||||
"user_viewable": 0,
|
||||
"user_editable": 0,
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"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.",
|
||||
"env_variable": "BUILD_NUMBER",
|
||||
"default_value": "latest",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|max:20"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2017-11-03T22:20:03-05:00",
|
||||
"exported_at": "2020-10-19T23:26:54+00:00",
|
||||
"name": "Sponge (SpongeVanilla)",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.",
|
||||
|
@ -17,9 +17,9 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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}",
|
||||
"container": "alpine:3.9",
|
||||
"entrypoint": "ash"
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The version of SpongeVanilla to download and use.",
|
||||
"env_variable": "SPONGE_VERSION",
|
||||
"default_value": "1.11.2-6.1.0-BETA-21",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/"
|
||||
},
|
||||
{
|
||||
|
@ -37,8 +37,8 @@
|
|||
"description": "The name of the Jarfile to use when running SpongeVanilla.",
|
||||
"env_variable": "SERVER_JARFILE",
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2019-12-25T19:55:01-05:00",
|
||||
"exported_at": "2020-10-23T23:04:17+00:00",
|
||||
"name": "Vanilla Minecraft",
|
||||
"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.",
|
||||
|
@ -17,9 +17,9 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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\"",
|
||||
"container": "alpine:3.10",
|
||||
"entrypoint": "ash"
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The name of the server jarfile to run the server with.",
|
||||
"env_variable": "SERVER_JARFILE",
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"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.",
|
||||
"env_variable": "VANILLA_VERSION",
|
||||
"default_value": "latest",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|between:3,15"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2018-01-21T16:58:36-06:00",
|
||||
"exported_at": "2020-10-20T00:03:09+00:00",
|
||||
"name": "Rust",
|
||||
"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.",
|
||||
|
@ -17,8 +17,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "ubuntu:16.04",
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The name of your server in the public server list.",
|
||||
"env_variable": "HOSTNAME",
|
||||
"default_value": "A Rust Server",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"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.",
|
||||
"env_variable": "OXIDE",
|
||||
"default_value": "0",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean"
|
||||
},
|
||||
{
|
||||
|
@ -46,8 +46,8 @@
|
|||
"description": "The world file for Rust to use.",
|
||||
"env_variable": "LEVEL",
|
||||
"default_value": "Procedural Map",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"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.",
|
||||
"env_variable": "DESCRIPTION",
|
||||
"default_value": "Powered by Pterodactyl",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string"
|
||||
},
|
||||
{
|
||||
|
@ -64,8 +64,8 @@
|
|||
"description": "The URL for your server. This is what comes up when clicking the \"Visit Website\" button.",
|
||||
"env_variable": "SERVER_URL",
|
||||
"default_value": "http:\/\/pterodactyl.io",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url"
|
||||
},
|
||||
{
|
||||
|
@ -73,8 +73,8 @@
|
|||
"description": "The world size for a procedural map.",
|
||||
"env_variable": "WORLD_SIZE",
|
||||
"default_value": "3000",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer"
|
||||
},
|
||||
{
|
||||
|
@ -82,8 +82,8 @@
|
|||
"description": "The seed for a procedural map.",
|
||||
"env_variable": "WORLD_SEED",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string"
|
||||
},
|
||||
{
|
||||
|
@ -91,8 +91,8 @@
|
|||
"description": "The maximum amount of players allowed in the server at once.",
|
||||
"env_variable": "MAX_PLAYERS",
|
||||
"default_value": "40",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer"
|
||||
},
|
||||
{
|
||||
|
@ -100,8 +100,8 @@
|
|||
"description": "The header image for the top of your server listing.",
|
||||
"env_variable": "SERVER_IMG",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url"
|
||||
},
|
||||
{
|
||||
|
@ -109,8 +109,8 @@
|
|||
"description": "Port for RCON connections.",
|
||||
"env_variable": "RCON_PORT",
|
||||
"default_value": "28016",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer"
|
||||
},
|
||||
{
|
||||
|
@ -118,17 +118,17 @@
|
|||
"description": "RCON access password.",
|
||||
"env_variable": "RCON_PASS",
|
||||
"default_value": "CHANGEME",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:64"
|
||||
},
|
||||
{
|
||||
"name": "Save Interval",
|
||||
"description": "Sets the server’s auto-save interval in seconds.",
|
||||
"description": "Sets the server\u2019s auto-save interval in seconds.",
|
||||
"env_variable": "SAVEINTERVAL",
|
||||
"default_value": "60",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer"
|
||||
},
|
||||
{
|
||||
|
@ -136,8 +136,8 @@
|
|||
"description": "Add additional startup parameters to the server.",
|
||||
"env_variable": "ADDITIONAL_ARGS",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,78 +3,87 @@
|
|||
"meta": {
|
||||
"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",
|
||||
"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",
|
||||
"image": "quay.io\/pterodactyl\/core: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\"",
|
||||
"image": "quay.io\/parkervcp\/pterodactyl-images:debian_source",
|
||||
"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": {
|
||||
"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}",
|
||||
"stop": "^C"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "ubuntu:16.04",
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"description": "If specified, players must provide this password to join the server.",
|
||||
"env_variable": "ARK_PASSWORD",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|alpha_dash|between:1,100"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"env_variable": "ARK_ADMIN_PASSWORD",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"default_value": "PleaseChangeMe",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|alpha_dash|between:1,100"
|
||||
},
|
||||
{
|
||||
"name": "Server Port",
|
||||
"description": "ARK server port used by client.",
|
||||
"env_variable": "PORT",
|
||||
"default_value": "7777",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"rules": "required|numeric"
|
||||
"name": "Server Map",
|
||||
"description": "Available Maps: TheIsland, TheCenter, Ragnarok, ScorchedEarth_P, Aberration_P, Extinction, Valguero_P, Genesis",
|
||||
"env_variable": "SERVER_MAP",
|
||||
"default_value": "TheIsland",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"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",
|
||||
"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",
|
||||
"default_value": "false",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"rules": "required|string|max:5"
|
||||
"default_value": "True",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|in:True,False"
|
||||
},
|
||||
{
|
||||
"name": "Rcon Port",
|
||||
"description": "ARK rcon port used by rcon tools.",
|
||||
"env_variable": "RCON_PORT",
|
||||
"default_value": "27020",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|numeric"
|
||||
},
|
||||
{
|
||||
|
@ -82,36 +91,27 @@
|
|||
"description": "ARK query port used by steam server browser and ark client server browser.",
|
||||
"env_variable": "QUERY_PORT",
|
||||
"default_value": "27015",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|numeric"
|
||||
},
|
||||
{
|
||||
"name": "Maximum Players",
|
||||
"description": "Specifies the maximum number of players that can play on the server simultaneously.",
|
||||
"env_variable": "SERVER_MAX_PLAYERS",
|
||||
"default_value": "20",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"rules": "required|numeric|digits_between:1,4"
|
||||
"name": "Auto-update server",
|
||||
"description": "This is to enable auto-updating for servers.\r\n\r\nDefault is 0. Set to 1 to update",
|
||||
"env_variable": "AUTO_UPDATE",
|
||||
"default_value": "0",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean"
|
||||
},
|
||||
{
|
||||
"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": 1,
|
||||
"user_editable": 0,
|
||||
"rules": "nullable|numeric"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"name": "Ballte Eye",
|
||||
"description": "Enable BattleEye\r\n\r\n0 to disable\r\n1 to enable\r\n\r\ndefault=\"1\"",
|
||||
"env_variable": "BATTLE_EYE",
|
||||
"default_value": "1",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"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",
|
||||
"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.",
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The default map for the server.",
|
||||
"env_variable": "SRCDS_MAP",
|
||||
"default_value": "de_dust2",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_dash"
|
||||
},
|
||||
{
|
||||
|
@ -37,8 +37,8 @@
|
|||
"description": "The Steam Account Token required for the server to be displayed publicly.",
|
||||
"env_variable": "STEAM_ACC",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_num|size:32"
|
||||
},
|
||||
{
|
||||
|
@ -46,8 +46,8 @@
|
|||
"description": "Required for game to update on server restart. Do not modify this.",
|
||||
"env_variable": "SRCDS_APPID",
|
||||
"default_value": "740",
|
||||
"user_viewable": 0,
|
||||
"user_editable": 0,
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|max:20"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"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",
|
||||
"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.",
|
||||
|
@ -17,8 +17,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "ubuntu:18.04",
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The ID corresponding to the game to download and run using SRCDS.",
|
||||
"env_variable": "SRCDS_APPID",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|numeric|digits_between:1,6"
|
||||
},
|
||||
{
|
||||
|
@ -37,8 +37,8 @@
|
|||
"description": "The name corresponding to the game to download and run using SRCDS.",
|
||||
"env_variable": "SRCDS_GAME",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|alpha_dash|between:1,100"
|
||||
},
|
||||
{
|
||||
|
@ -46,8 +46,8 @@
|
|||
"description": "The default map for the server.",
|
||||
"env_variable": "SRCDS_MAP",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_dash"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2019-12-08T10:56:42-05:00",
|
||||
"exported_at": "2020-10-19T23:34:44+00:00",
|
||||
"name": "Garrys Mod",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.",
|
||||
|
@ -17,8 +17,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "ubuntu:18.04",
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The default map for the server.",
|
||||
"env_variable": "SRCDS_MAP",
|
||||
"default_value": "gm_flatgrass",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_dash"
|
||||
},
|
||||
{
|
||||
|
@ -37,8 +37,8 @@
|
|||
"description": "The Steam Account Token required for the server to be displayed publicly.",
|
||||
"env_variable": "STEAM_ACC",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string|alpha_num|size:32"
|
||||
},
|
||||
{
|
||||
|
@ -46,8 +46,8 @@
|
|||
"description": "Required for game to update on server restart. Do not modify this.",
|
||||
"env_variable": "SRCDS_APPID",
|
||||
"default_value": "4020",
|
||||
"user_viewable": 0,
|
||||
"user_editable": 0,
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|max:20"
|
||||
},
|
||||
{
|
||||
|
@ -55,8 +55,8 @@
|
|||
"description": "The ID of your workshop collection (the numbers at the end of the URL)",
|
||||
"env_variable": "WORKSHOP_ID",
|
||||
"default_value": "",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|integer"
|
||||
},
|
||||
{
|
||||
|
@ -64,8 +64,8 @@
|
|||
"description": "The gamemode of your server.",
|
||||
"env_variable": "GAMEMODE",
|
||||
"default_value": "sandbox",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string"
|
||||
},
|
||||
{
|
||||
|
@ -73,8 +73,8 @@
|
|||
"description": "The maximum amount of players allowed on your game server.",
|
||||
"env_variable": "MAX_PLAYERS",
|
||||
"default_value": "32",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer|max:128"
|
||||
},
|
||||
{
|
||||
|
@ -82,8 +82,8 @@
|
|||
"description": "The tickrate defines how fast the server will update each entities location.",
|
||||
"env_variable": "TICKRATE",
|
||||
"default_value": "22",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer|max:100"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2019-12-08T10:57:32-05:00",
|
||||
"exported_at": "2020-10-19T23:35:42+00:00",
|
||||
"name": "Insurgency",
|
||||
"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.",
|
||||
|
@ -17,8 +17,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "ubuntu:18.04",
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The ID corresponding to the game to download and run using SRCDS.",
|
||||
"env_variable": "SRCDS_APPID",
|
||||
"default_value": "237410",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|regex:\/^(237410)$\/"
|
||||
},
|
||||
{
|
||||
|
@ -37,8 +37,8 @@
|
|||
"description": "The name corresponding to the game to download and run using SRCDS.",
|
||||
"env_variable": "SRCDS_GAME",
|
||||
"default_value": "insurgency",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|regex:\/^(insurgency)$\/"
|
||||
},
|
||||
{
|
||||
|
@ -46,8 +46,8 @@
|
|||
"description": "The default map to use when starting the server.",
|
||||
"env_variable": "SRCDS_MAP",
|
||||
"default_value": "sinjar",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^(\\w{1,20})$\/"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"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",
|
||||
"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.",
|
||||
|
@ -17,8 +17,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "ubuntu:18.04",
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The ID corresponding to the game to download and run using SRCDS.",
|
||||
"env_variable": "SRCDS_APPID",
|
||||
"default_value": "232250",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|regex:\/^(232250)$\/"
|
||||
},
|
||||
{
|
||||
|
@ -37,8 +37,8 @@
|
|||
"description": "The name corresponding to the game to download and run using SRCDS.",
|
||||
"env_variable": "SRCDS_GAME",
|
||||
"default_value": "tf",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|regex:\/^(tf)$\/"
|
||||
},
|
||||
{
|
||||
|
@ -46,8 +46,8 @@
|
|||
"description": "The default map to use when starting the server.",
|
||||
"env_variable": "SRCDS_MAP",
|
||||
"default_value": "cp_dustbowl",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^(\\w{1,20})$\/"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2018-01-21T17:01:44-06:00",
|
||||
"exported_at": "2020-10-25T22:34:06+00:00",
|
||||
"name": "Mumble Server",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "Mumble is an open source, low-latency, high quality voice chat software primarily intended for use while gaming.",
|
||||
|
@ -17,9 +17,9 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "alpine:3.9",
|
||||
"entrypoint": "ash"
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
|
@ -28,17 +28,17 @@
|
|||
"description": "Maximum concurrent users on the mumble server.",
|
||||
"env_variable": "MAX_USERS",
|
||||
"default_value": "100",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|numeric|digits_between:1,5"
|
||||
},
|
||||
{
|
||||
"name": "Server Version",
|
||||
"description": "Version of Mumble Server to download and use.",
|
||||
"env_variable": "MUMBLE_VERSION",
|
||||
"default_value": "1.3.1",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,23 +3,23 @@
|
|||
"meta": {
|
||||
"version": "PTDL_v1"
|
||||
},
|
||||
"exported_at": "2019-07-05T11:59:29-04:00",
|
||||
"exported_at": "2020-10-25T22:24:15+00:00",
|
||||
"name": "Teamspeak3 Server",
|
||||
"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.",
|
||||
"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",
|
||||
"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",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"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"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"container": "alpine:3.9",
|
||||
"entrypoint": "ash"
|
||||
"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": "debian:buster-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
|
@ -28,8 +28,8 @@
|
|||
"description": "The version of Teamspeak 3 to use when running the server.",
|
||||
"env_variable": "TS_VERSION",
|
||||
"default_value": "latest",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 1,
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:6"
|
||||
},
|
||||
{
|
||||
|
@ -37,8 +37,8 @@
|
|||
"description": "The Teamspeak file transfer port",
|
||||
"env_variable": "FILE_TRANSFER",
|
||||
"default_value": "30033",
|
||||
"user_viewable": 1,
|
||||
"user_editable": 0,
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer|between:1,65535"
|
||||
}
|
||||
]
|
||||
|
|
48
package.json
48
package.json
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"name": "pterodactyl-panel",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.19",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.9.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.11",
|
||||
"@types/react-copy-to-clipboard": "^4.3.0",
|
||||
"axios": "^0.19.2",
|
||||
"chart.js": "^2.8.0",
|
||||
"codemirror": "^5.57.0",
|
||||
|
@ -21,11 +22,12 @@
|
|||
"path": "^0.12.7",
|
||||
"query-string": "^6.7.0",
|
||||
"react": "^16.13.1",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "npm:@hot-loader/react-dom",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-ga": "^3.1.2",
|
||||
"react-google-recaptcha": "^2.0.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-ga": "^3.1.2",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-i18next": "^11.2.1",
|
||||
"react-redux": "^7.1.0",
|
||||
|
@ -37,29 +39,31 @@
|
|||
"styled-components-breakpoint": "^3.0.0-preview.20",
|
||||
"swr": "^0.2.3",
|
||||
"uuid": "^3.3.2",
|
||||
"xterm": "^3.14.4",
|
||||
"xterm-addon-attach": "^0.1.0",
|
||||
"xterm-addon-fit": "^0.1.0",
|
||||
"xterm": "^4.9.0",
|
||||
"xterm-addon-attach": "^0.6.0",
|
||||
"xterm-addon-fit": "^0.4.0",
|
||||
"xterm-addon-search": "^0.7.0",
|
||||
"xterm-addon-search-bar": "^0.2.0",
|
||||
"yup": "^0.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-react-jsx": "^7.10.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.5",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/runtime": "^7.7.5",
|
||||
"@babel/core": "^7.12.1",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-react-jsx": "^7.12.1",
|
||||
"@babel/plugin-transform-runtime": "^7.12.1",
|
||||
"@babel/preset-env": "^7.12.1",
|
||||
"@babel/preset-react": "^7.12.1",
|
||||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@babel/runtime": "^7.12.1",
|
||||
"@types/chart.js": "^2.8.5",
|
||||
"@types/codemirror": "^0.0.98",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/events": "^3.0.0",
|
||||
"@types/node": "^12.6.9",
|
||||
"@types/node": "^14.11.10",
|
||||
"@types/query-string": "^6.3.0",
|
||||
"@types/react": "^16.9.41",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
|
@ -91,9 +95,9 @@
|
|||
"source-map-loader": "^1.0.1",
|
||||
"style-loader": "^1.2.1",
|
||||
"svg-url-loader": "^6.0.0",
|
||||
"tailwindcss": "^1.4.6",
|
||||
"tailwindcss": "^1.9.4",
|
||||
"terser-webpack-plugin": "^3.0.6",
|
||||
"twin.macro": "^1.4.1",
|
||||
"twin.macro": "^1.10.0",
|
||||
"typescript": "^3.9.6",
|
||||
"typescript-plugin-tw-template": "^2.0.1",
|
||||
"webpack": "^4.43.0",
|
||||
|
|
|
@ -14,7 +14,7 @@ return [
|
|||
'deleted' => 'Successfully deleted the requested location.',
|
||||
],
|
||||
'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)',
|
||||
'deleted' => 'User successfully deleted from the Panel.',
|
||||
'confirm_delete' => 'Are you sure you want to delete this user from the Panel?',
|
||||
|
|
|
@ -4,16 +4,15 @@ import http, { getPaginationSet, PaginatedResult } from '@/api/http';
|
|||
interface QueryParams {
|
||||
query?: string;
|
||||
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) => {
|
||||
http.get('/api/client', {
|
||||
params: {
|
||||
type: onlyAdmin ? 'admin' : undefined,
|
||||
'filter[name]': query,
|
||||
page,
|
||||
'filter[*]': query,
|
||||
...params,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => resolve({
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export default (uuid: string, file: string, content: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(
|
||||
`/api/client/servers/${uuid}/files/write`,
|
||||
content,
|
||||
{
|
||||
params: { file },
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
},
|
||||
)
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
export default async (uuid: string, file: string, content: string): Promise<void> => {
|
||||
await http.post(`/api/client/servers/${uuid}/files/write`, content, {
|
||||
params: { file },
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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`);
|
|
@ -63,7 +63,7 @@ export default () => {
|
|||
<FontAwesomeIcon icon={faUserCircle}/>
|
||||
</NavLink>
|
||||
{rootAdmin &&
|
||||
<a href={'/admin'} target={'_blank'} rel={'noreferrer'}>
|
||||
<a href={'/admin'} rel={'noreferrer'}>
|
||||
<FontAwesomeIcon icon={faCogs}/>
|
||||
</a>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
|
@ -24,13 +24,23 @@ export default () => {
|
|||
const { clearFlashes, addFlash } = useFlash();
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes();
|
||||
}, []);
|
||||
|
||||
const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
|
||||
clearFlashes();
|
||||
|
||||
// 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.
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -43,7 +53,12 @@ export default () => {
|
|||
console.error(error);
|
||||
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
.then(() => {
|
||||
setToken('');
|
||||
if (ref.current) ref.current.reset();
|
||||
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import loginCheckpoint from '@/api/auth/loginCheckpoint';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { ActionCreator } from 'easy-peasy';
|
||||
import { StaticContext } from 'react-router';
|
||||
|
@ -20,8 +19,7 @@ interface Values {
|
|||
type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }>
|
||||
|
||||
type Props = OwnProps & {
|
||||
addError: ActionCreator<FlashStore['addError']['payload']>;
|
||||
clearFlashes: ActionCreator<FlashStore['clearFlashes']['payload']>;
|
||||
clearAndAddHttpError: ActionCreator<FlashStore['clearAndAddHttpError']['payload']>;
|
||||
}
|
||||
|
||||
const LoginCheckpointContainer = () => {
|
||||
|
@ -79,9 +77,7 @@ const LoginCheckpointContainer = () => {
|
|||
};
|
||||
|
||||
const EnhancedForm = withFormik<Props, Values>({
|
||||
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { addError, clearFlashes, location } }) => {
|
||||
clearFlashes();
|
||||
|
||||
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { clearAndAddHttpError, location } }) => {
|
||||
loginCheckpoint(location.state?.token || '', code, recoveryCode)
|
||||
.then(response => {
|
||||
if (response.complete) {
|
||||
|
@ -95,7 +91,7 @@ const EnhancedForm = withFormik<Props, Values>({
|
|||
.catch(error => {
|
||||
console.error(error);
|
||||
setSubmitting(false);
|
||||
addError({ message: httpErrorToHuman(error) });
|
||||
clearAndAddHttpError({ error });
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -106,7 +102,7 @@ const EnhancedForm = withFormik<Props, Values>({
|
|||
})(LoginCheckpointContainer);
|
||||
|
||||
export default ({ history, location, ...props }: OwnProps) => {
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
const { clearAndAddHttpError } = useFlash();
|
||||
|
||||
if (!location.state?.token) {
|
||||
history.replace('/auth/login');
|
||||
|
@ -115,8 +111,7 @@ export default ({ history, location, ...props }: OwnProps) => {
|
|||
}
|
||||
|
||||
return <EnhancedForm
|
||||
addError={addError}
|
||||
clearFlashes={clearFlashes}
|
||||
clearAndAddHttpError={clearAndAddHttpError}
|
||||
history={history}
|
||||
location={location}
|
||||
{...props}
|
||||
|
|
|
@ -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 login from '@/api/auth/login';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
|
@ -23,13 +23,23 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes();
|
||||
}, []);
|
||||
|
||||
const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes();
|
||||
|
||||
// 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.
|
||||
if (recaptchaEnabled && !token) {
|
||||
ref.current!.execute().catch(error => console.error(error));
|
||||
ref.current!.execute().catch(error => {
|
||||
console.error(error);
|
||||
|
||||
setSubmitting(false);
|
||||
clearAndAddHttpError({ error });
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -46,6 +56,9 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
setToken('');
|
||||
if (ref.current) ref.current.reset();
|
||||
|
||||
setSubmitting(false);
|
||||
clearAndAddHttpError({ error });
|
||||
});
|
||||
|
@ -63,23 +76,23 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||
{({ isSubmitting, setSubmitting, submitForm }) => (
|
||||
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}>
|
||||
<Field
|
||||
light
|
||||
type={'text'}
|
||||
label={'Username or Email'}
|
||||
id={'username'}
|
||||
name={'username'}
|
||||
light
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
light
|
||||
type={'password'}
|
||||
label={'Password'}
|
||||
id={'password'}
|
||||
name={'password'}
|
||||
light
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting} disabled={isSubmitting}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,7 @@ export default () => {
|
|||
|
||||
const { data: servers, error } = useSWR<PaginatedResult<Server>>(
|
||||
[ '/api/client/servers', showOnlyAdmin, page ],
|
||||
() => getServers({ onlyAdmin: showOnlyAdmin, page }),
|
||||
() => getServers({ page, type: showOnlyAdmin ? 'admin' : undefined }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -41,8 +41,9 @@ const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | unde
|
|||
|
||||
export default ({ server, className }: { server: Server; className?: string }) => {
|
||||
const interval = useRef<number>(null);
|
||||
const [ stats, setStats ] = useState<ServerStats | null>(null);
|
||||
const [ statsError, setStatsError ] = useState(false);
|
||||
const [ isSuspended, setIsSuspended ] = useState(server.isSuspended);
|
||||
const [ stats, setStats ] = useState<ServerStats | null>(null);
|
||||
|
||||
const getStats = () => {
|
||||
setStatsError(false);
|
||||
|
@ -55,6 +56,14 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
};
|
||||
|
||||
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(() => {
|
||||
// @ts-ignore
|
||||
interval.current = setInterval(() => getStats(), 20000);
|
||||
|
@ -63,7 +72,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
return () => {
|
||||
interval.current && clearInterval(interval.current);
|
||||
};
|
||||
}, []);
|
||||
}, [ isSuspended ]);
|
||||
|
||||
const alarms = { cpu: false, memory: false, disk: false };
|
||||
if (stats) {
|
||||
|
@ -101,9 +110,13 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
</p>
|
||||
</div>
|
||||
<div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}>
|
||||
{!stats ?
|
||||
!statsError ?
|
||||
<Spinner size={'small'}/>
|
||||
{(!stats || isSuspended) ?
|
||||
isSuspended ?
|
||||
<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 ?
|
||||
<div css={tw`flex-1 text-center`}>
|
||||
|
@ -112,11 +125,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
</span>
|
||||
</div>
|
||||
:
|
||||
<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>
|
||||
<Spinner size={'small'}/>
|
||||
:
|
||||
<React.Fragment>
|
||||
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>
|
||||
|
|
|
@ -7,7 +7,6 @@ import { object, string } from 'yup';
|
|||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
|
@ -16,11 +15,10 @@ interface Values {
|
|||
}
|
||||
|
||||
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 submit = ({ password }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('account:two-factor');
|
||||
disableAccountTwoFactor(password)
|
||||
.then(() => {
|
||||
updateUserData({ useTotp: false });
|
||||
|
@ -29,7 +27,7 @@ export default ({ ...props }: RequiredModalProps) => {
|
|||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
|
||||
clearAndAddHttpError({ error, key: 'account:two-factor' });
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,7 +6,6 @@ import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
|
|||
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import Field from '@/components/elements/Field';
|
||||
import tw from 'twin.macro';
|
||||
|
@ -22,20 +21,18 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
|||
const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]);
|
||||
|
||||
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(() => {
|
||||
clearFlashes('account:two-factor');
|
||||
getTwoFactorTokenUrl()
|
||||
.then(setToken)
|
||||
.catch(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>) => {
|
||||
clearFlashes('account:two-factor');
|
||||
enableAccountTwoFactor(code)
|
||||
.then(tokens => {
|
||||
setRecoveryTokens(tokens);
|
||||
|
@ -43,7 +40,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
|||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
|
||||
clearAndAddHttpError({ error, key: 'account:two-factor' });
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
|
|
|
@ -9,7 +9,6 @@ import InputSpinner from '@/components/elements/InputSpinner';
|
|||
import getServers from '@/api/getServers';
|
||||
import { Server } from '@/api/server/getServer';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
|
@ -47,88 +46,86 @@ const SearchWatcher = () => {
|
|||
|
||||
export default ({ ...props }: Props) => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ servers, setServers ] = useState<Server[]>([]);
|
||||
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>) => {
|
||||
setLoading(true);
|
||||
setSubmitting(false);
|
||||
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)))
|
||||
.catch(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);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.visible) {
|
||||
setTimeout(() => ref.current?.focus(), 250);
|
||||
if (ref.current) ref.current.focus();
|
||||
}
|
||||
}, [ props.visible ]);
|
||||
|
||||
// Formik does not support an innerRef on custom components.
|
||||
const InputWithRef = (props: any) => <Input autoFocus {...props} ref={ref}/>;
|
||||
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={search}
|
||||
validationSchema={object().shape({
|
||||
term: string()
|
||||
.min(3, 'Please enter at least three characters to begin searching.')
|
||||
.required('A search term must be provided.'),
|
||||
term: string().min(3, 'Please enter at least three characters to begin searching.'),
|
||||
})}
|
||||
initialValues={{ term: '' } as Values}
|
||||
>
|
||||
<Modal {...props}>
|
||||
<Form>
|
||||
<FormikFieldWrapper
|
||||
name={'term'}
|
||||
label={'Search term'}
|
||||
description={
|
||||
isAdmin
|
||||
? 'Enter a server name, user email, or uuid to begin searching.'
|
||||
: 'Enter a server name to begin searching.'
|
||||
{({ isSubmitting }) => (
|
||||
<Modal {...props}>
|
||||
<Form>
|
||||
<FormikFieldWrapper
|
||||
name={'term'}
|
||||
label={'Search term'}
|
||||
description={'Enter a server name, uuid, or allocation to begin searching.'}
|
||||
>
|
||||
<SearchWatcher/>
|
||||
<InputSpinner visible={isSubmitting}>
|
||||
<Field as={InputWithRef} name={'term'}/>
|
||||
</InputSpinner>
|
||||
</FormikFieldWrapper>
|
||||
</Form>
|
||||
{servers.length > 0 &&
|
||||
<div css={tw`mt-6`}>
|
||||
{
|
||||
servers.map(server => (
|
||||
<ServerResult
|
||||
key={server.uuid}
|
||||
to={`/server/${server.id}`}
|
||||
onClick={() => props.onDismissed()}
|
||||
>
|
||||
<div css={tw`flex-1 mr-4`}>
|
||||
<p css={tw`text-sm`}>{server.name}</p>
|
||||
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div css={tw`flex-none text-right`}>
|
||||
<span css={tw`text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded`}>
|
||||
{server.node}
|
||||
</span>
|
||||
</div>
|
||||
</ServerResult>
|
||||
))
|
||||
}
|
||||
>
|
||||
<SearchWatcher/>
|
||||
<InputSpinner visible={loading}>
|
||||
<Field as={Input} innerRef={ref} name={'term'}/>
|
||||
</InputSpinner>
|
||||
</FormikFieldWrapper>
|
||||
</Form>
|
||||
{servers.length > 0 &&
|
||||
<div css={tw`mt-6`}>
|
||||
{
|
||||
servers.map(server => (
|
||||
<ServerResult
|
||||
key={server.uuid}
|
||||
to={`/server/${server.id}`}
|
||||
onClick={() => props.onDismissed()}
|
||||
>
|
||||
<div>
|
||||
<p css={tw`text-sm`}>{server.name}</p>
|
||||
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div css={tw`flex-1 text-right`}>
|
||||
<span css={tw`text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded`}>
|
||||
{server.node}
|
||||
</span>
|
||||
</div>
|
||||
</ServerResult>
|
||||
))
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Modal>
|
||||
</Modal>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -57,8 +57,8 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
|||
`};
|
||||
`};
|
||||
|
||||
${props => props.size === 'xsmall' && tw`p-2 text-xs`};
|
||||
${props => (!props.size || props.size === 'small') && tw`p-3`};
|
||||
${props => props.size === 'xsmall' && tw`px-2 py-1 text-xs`};
|
||||
${props => (!props.size || props.size === 'small') && tw`px-4 py-2`};
|
||||
${props => props.size === 'large' && tw`p-4 text-sm`};
|
||||
${props => props.size === 'xlarge' && tw`p-4 w-full`};
|
||||
|
||||
|
|
63
resources/scripts/components/elements/CopyOnClick.tsx
Normal file
63
resources/scripts/components/elements/CopyOnClick.tsx
Normal 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 "{text}" to clipboard.</p>
|
||||
</div>
|
||||
</Toast>
|
||||
:
|
||||
<></>
|
||||
}
|
||||
</Fade>
|
||||
</SwitchTransition>
|
||||
<CopyToClipboard onCopy={onCopy} text={text} options={{ debug: true }}>
|
||||
{children}
|
||||
</CopyToClipboard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyOnClick;
|
39
resources/scripts/components/elements/ErrorBoundary.tsx
Normal file
39
resources/scripts/components/elements/ErrorBoundary.tsx
Normal 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;
|
31
resources/scripts/components/elements/Icon.tsx
Normal file
31
resources/scripts/components/elements/Icon.tsx
Normal 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;
|
|
@ -5,12 +5,7 @@ import tw from 'twin.macro';
|
|||
|
||||
const InputSpinner = ({ visible, children }: { visible: boolean, children: React.ReactNode }) => (
|
||||
<div css={tw`relative`}>
|
||||
<Fade
|
||||
appear
|
||||
unmountOnExit
|
||||
in={visible}
|
||||
timeout={150}
|
||||
>
|
||||
<Fade appear unmountOnExit in={visible} timeout={150}>
|
||||
<div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}>
|
||||
<Spinner size={'small'}/>
|
||||
</div>
|
||||
|
|
|
@ -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 tw from 'twin.macro';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
import { breakpoint } from '@/theme';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
export interface RequiredModalProps {
|
||||
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;
|
||||
|
|
|
@ -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 * 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 { ServerContext } from '@/state/server';
|
||||
import styled from 'styled-components/macro';
|
||||
import { usePermissions } from '@/plugins/usePermissions';
|
||||
import tw from 'twin.macro';
|
||||
import 'xterm/dist/xterm.css';
|
||||
import tw, { theme as th } from 'twin.macro';
|
||||
import 'xterm/css/xterm.css';
|
||||
import useEventListener from '@/plugins/useEventListener';
|
||||
import { debounce } from 'debounce';
|
||||
import { usePersistedState } from '@/plugins/usePersistedState';
|
||||
|
||||
const theme = {
|
||||
background: 'transparent',
|
||||
background: th`colors.black`.toString(),
|
||||
cursor: 'transparent',
|
||||
black: '#000000',
|
||||
black: th`colors.black`.toString(),
|
||||
red: '#E54B4B',
|
||||
green: '#9ECE58',
|
||||
yellow: '#FAED70',
|
||||
|
@ -29,6 +32,7 @@ const theme = {
|
|||
brightMagenta: '#C792EA',
|
||||
brightCyan: '#89DDFF',
|
||||
brightWhite: '#ffffff',
|
||||
selection: '#FAF089',
|
||||
};
|
||||
|
||||
const terminalProps: ITerminalOptions = {
|
||||
|
@ -55,8 +59,14 @@ export default () => {
|
|||
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pterodactyl~ \u001b[0m';
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
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 [ 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(
|
||||
(prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
||||
|
@ -71,37 +81,61 @@ export default () => {
|
|||
);
|
||||
|
||||
const handleCommandKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== 'Enter' || (e.key === 'Enter' && e.currentTarget.value.length < 1)) {
|
||||
return;
|
||||
if (e.key === 'ArrowUp') {
|
||||
const newIndex = Math.min(historyIndex + 1, history!.length - 1);
|
||||
|
||||
setHistoryIndex(newIndex);
|
||||
e.currentTarget.value = history![newIndex] || '';
|
||||
}
|
||||
|
||||
instance && instance.send('send command', e.currentTarget.value);
|
||||
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 = '';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (connected && ref.current && !terminal.element) {
|
||||
terminal.open(ref.current);
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(searchAddon);
|
||||
terminal.loadAddon(searchBar);
|
||||
fitAddon.fit();
|
||||
|
||||
// @see https://github.com/xtermjs/xterm.js/issues/2265
|
||||
// @see https://github.com/xtermjs/xterm.js/issues/2230
|
||||
TerminalFit.fit(terminal);
|
||||
|
||||
// Add support for copying terminal text.
|
||||
// Add support for capturing keys
|
||||
terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
|
||||
// Ctrl + C
|
||||
if (e.ctrlKey && (e.key === 'c')) {
|
||||
// Ctrl + C ( Copy )
|
||||
if (e.ctrlKey && e.key === 'c') {
|
||||
document.execCommand('copy');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.ctrlKey && e.key === 'f') {
|
||||
searchBar.show();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
searchBar.hidden();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}, [ terminal, connected ]);
|
||||
|
||||
const fit = debounce(() => {
|
||||
TerminalFit.fit(terminal);
|
||||
fitAddon.fit();
|
||||
}, 100);
|
||||
|
||||
useEventListener('resize', () => fit());
|
||||
|
|
|
@ -16,6 +16,7 @@ import Select from '@/components/elements/Select';
|
|||
import modes from '@/modes';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||
|
||||
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
|
||||
|
||||
|
@ -60,9 +61,7 @@ export default () => {
|
|||
setLoading(true);
|
||||
clearFlashes('files:view');
|
||||
fetchFileContent()
|
||||
.then(content => {
|
||||
return saveFileContents(uuid, name || hash.replace(/^#/, ''), content);
|
||||
})
|
||||
.then(content => saveFileContents(uuid, encodeURIComponent(name || hash.replace(/^#/, '')), content))
|
||||
.then(() => {
|
||||
if (name) {
|
||||
history.push(`/server/${id}/files/edit#/${name}`);
|
||||
|
@ -87,7 +86,9 @@ export default () => {
|
|||
return (
|
||||
<PageContentBlock>
|
||||
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
|
||||
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
|
||||
<ErrorBoundary>
|
||||
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
|
||||
</ErrorBoundary>
|
||||
{hash.replace(/^#/, '').endsWith('.pteroignore') &&
|
||||
<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`}>
|
||||
|
|
|
@ -33,10 +33,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
|||
.filter(directory => !!directory)
|
||||
.map((directory, index, dirs) => {
|
||||
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>) => {
|
||||
|
@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
|||
}
|
||||
{file &&
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,7 @@ import MassActionsBar from '@/components/server/files/MassActionsBar';
|
|||
import UploadButton from '@/components/server/files/UploadButton';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import { useStoreActions } from '@/state/hooks';
|
||||
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||
|
||||
const sortFiles = (files: FileObject[]): FileObject[] => {
|
||||
return files.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
@ -50,7 +51,9 @@ export default () => {
|
|||
|
||||
return (
|
||||
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
|
||||
<FileManagerBreadcrumbs/>
|
||||
<ErrorBoundary>
|
||||
<FileManagerBreadcrumbs/>
|
||||
</ErrorBoundary>
|
||||
{
|
||||
!files ?
|
||||
<Spinner size={'large'} centered/>
|
||||
|
@ -81,18 +84,20 @@ export default () => {
|
|||
</CSSTransition>
|
||||
}
|
||||
<Can action={'file.create'}>
|
||||
<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`}/>
|
||||
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
|
||||
<NavLink
|
||||
to={`/server/${id}/files/new${window.location.hash}`}
|
||||
css={tw`flex-1 sm:flex-none sm:mt-0`}
|
||||
>
|
||||
<Button css={tw`w-full`}>
|
||||
New File
|
||||
</Button>
|
||||
</NavLink>
|
||||
</div>
|
||||
<ErrorBoundary>
|
||||
<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`}/>
|
||||
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
|
||||
<NavLink
|
||||
to={`/server/${id}/files/new${window.location.hash}`}
|
||||
css={tw`flex-1 sm:flex-none sm:mt-0`}
|
||||
>
|
||||
<Button css={tw`w-full`}>
|
||||
New File
|
||||
</Button>
|
||||
</NavLink>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</Can>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -92,9 +92,9 @@ export default ({ className }: WithClassname) => {
|
|||
<span css={tw`text-neutral-200`}>This directory will be created as</span>
|
||||
/home/container/
|
||||
<span css={tw`text-cyan-200`}>
|
||||
{decodeURIComponent(
|
||||
{decodeURIComponent(encodeURIComponent(
|
||||
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
|
||||
)}
|
||||
))}
|
||||
</span>
|
||||
</p>
|
||||
<div css={tw`flex justify-end`}>
|
||||
|
|
|
@ -14,6 +14,7 @@ import { debounce } from 'debounce';
|
|||
import setServerAllocationNotes from '@/api/server/network/setServerAllocationNotes';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
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 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`}>
|
||||
<div css={tw`flex items-center w-full md:w-auto`}>
|
||||
<div css={tw`pl-4 pr-6 text-neutral-400`}>
|
||||
<FontAwesomeIcon icon={faNetworkWired}/>
|
||||
<FontAwesomeIcon icon={faNetworkWired} />
|
||||
</div>
|
||||
<div css={tw`mr-4 flex-1 md:w-40`}>
|
||||
<Code>{allocation.alias || allocation.ip}</Code>
|
||||
<Label>IP Address</Label>
|
||||
{allocation.alias ? <CopyOnClick text={allocation.alias}><Code css={tw`w-40 truncate`}>{allocation.alias}</Code></CopyOnClick> :
|
||||
<CopyOnClick text={allocation.ip}><Code>{allocation.ip}</Code></CopyOnClick>}
|
||||
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
||||
</div>
|
||||
<div css={tw`w-16 md:w-24 overflow-hidden`}>
|
||||
<Code>{allocation.port}</Code>
|
||||
|
|
|
@ -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
|
||||
will be terminated.
|
||||
</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
|
||||
</Button>
|
||||
</>
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||
import { Schedule } from '@/api/server/schedules/getServerSchedules';
|
||||
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
|
||||
import Button from '@/components/elements/Button';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
interface Props {
|
||||
schedule: Schedule;
|
||||
|
@ -18,7 +19,7 @@ export default ({ schedule }: Props) => {
|
|||
onDismissed={() => setVisible(false)}
|
||||
/>
|
||||
}
|
||||
<Button onClick={() => setVisible(true)}>
|
||||
<Button onClick={() => setVisible(true)} css={tw`flex-1`}>
|
||||
New Task
|
||||
</Button>
|
||||
</>
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,12 +1,9 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { Schedule } from '@/api/server/schedules/getServerSchedules';
|
||||
import getServerSchedule from '@/api/server/schedules/getServerSchedule';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
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 NewTaskButton from '@/components/server/schedules/NewTaskButton';
|
||||
import DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton';
|
||||
|
@ -16,7 +13,11 @@ import { ServerContext } from '@/state/server';
|
|||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import tw from 'twin.macro';
|
||||
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 {
|
||||
id: string;
|
||||
|
@ -26,15 +27,34 @@ interface State {
|
|||
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>) => {
|
||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
|
||||
const { clearFlashes, addError } = useFlash();
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const [ isLoading, setIsLoading ] = useState(true);
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -48,11 +68,15 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
|||
.then(schedule => appendSchedule(schedule))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addError({ message: httpErrorToHuman(error), key: 'schedules' });
|
||||
clearAndAddHttpError({ error, key: 'schedules' });
|
||||
})
|
||||
.then(() => setIsLoading(false));
|
||||
}, [ match ]);
|
||||
|
||||
const toggleEditModal = useCallback(() => {
|
||||
setShowEditModal(s => !s);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContentBlock>
|
||||
<FlashMessageRender byKey={'schedules'} css={tw`mb-4`}/>
|
||||
|
@ -60,52 +84,73 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
|||
<Spinner size={'large'} centered/>
|
||||
:
|
||||
<>
|
||||
<GreyRowBox css={tw`cursor-pointer mb-2 flex-wrap`}>
|
||||
<ScheduleRow schedule={schedule}/>
|
||||
</GreyRowBox>
|
||||
<EditScheduleModal
|
||||
visible={showEditModal}
|
||||
schedule={schedule}
|
||||
onDismissed={() => setShowEditModal(false)}
|
||||
/>
|
||||
<div css={tw`flex items-center mt-8 mb-4`}>
|
||||
<div css={tw`flex-1`}>
|
||||
<h2 css={tw`text-2xl`}>Configured Tasks</h2>
|
||||
<ScheduleCronRow cron={schedule.cron} css={tw`sm:hidden bg-neutral-700 rounded mb-4 p-3`}/>
|
||||
<div css={tw`hidden sm:grid grid-cols-5 md:grid-cols-7 gap-4 mb-6`}>
|
||||
<CronBox title={'Minute'} value={schedule.cron.minute}/>
|
||||
<CronBox title={'Hour'} value={schedule.cron.hour}/>
|
||||
<CronBox title={'Day (Month)'} value={schedule.cron.dayOfMonth}/>
|
||||
<CronBox title={'Month'} value={'*'}/>
|
||||
<CronBox title={'Day (Week)'} value={schedule.cron.dayOfWeek}/>
|
||||
</div>
|
||||
<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`}>
|
||||
<h3 css={tw`flex items-center text-neutral-100 text-2xl`}>
|
||||
{schedule.name}
|
||||
{schedule.isProcessing ?
|
||||
<span
|
||||
css={tw`flex items-center rounded-full px-2 py-px text-xs ml-4 uppercase bg-neutral-600 text-white`}
|
||||
>
|
||||
<Spinner css={tw`w-3! h-3! mr-2`}/>
|
||||
Processing
|
||||
</span>
|
||||
:
|
||||
<ActivePill active={schedule.isActive}/>
|
||||
}
|
||||
</h3>
|
||||
<p css={tw`mt-1 text-sm text-neutral-300`}>
|
||||
Last run at:
|
||||
{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>
|
||||
{schedule.tasks.length > 0 ?
|
||||
<>
|
||||
{
|
||||
schedule.tasks
|
||||
.sort((a, b) => a.sequenceId - b.sequenceId)
|
||||
.map(task => (
|
||||
<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`}>
|
||||
There are no tasks configured for this schedule.
|
||||
</p>
|
||||
}
|
||||
<div css={tw`mt-8 flex justify-end`}>
|
||||
<EditScheduleModal visible={showEditModal} schedule={schedule} onDismissed={toggleEditModal}/>
|
||||
<div css={tw`mt-6 flex sm:justify-end`}>
|
||||
<Can action={'schedule.delete'}>
|
||||
<DeleteScheduleButton
|
||||
scheduleId={schedule.id}
|
||||
onDeleted={() => history.push(`/server/${id}/schedules`)}
|
||||
/>
|
||||
</Can>
|
||||
{schedule.isActive && schedule.tasks.length > 0 &&
|
||||
<Can action={'schedule.update'}>
|
||||
<Button css={tw`mr-4`} onClick={() => setShowEditModal(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
<NewTaskButton schedule={schedule}/>
|
||||
<RunScheduleButton schedule={schedule}/>
|
||||
</Can>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import { faCalendarAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { format } from 'date-fns';
|
||||
import tw from 'twin.macro';
|
||||
import ScheduleCronRow from '@/components/server/schedules/ScheduleCronRow';
|
||||
|
||||
export default ({ schedule }: { schedule: Schedule }) => (
|
||||
<>
|
||||
|
@ -27,36 +28,19 @@ export default ({ schedule }: { schedule: Schedule }) => (
|
|||
{schedule.isActive ? 'Active' : 'Inactive'}
|
||||
</p>
|
||||
</div>
|
||||
<div css={tw`flex items-center 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>
|
||||
<ScheduleCronRow cron={schedule.cron} css={tw`mx-auto sm:mx-8 w-full sm:w-auto mt-4 sm:mt-0`}/>
|
||||
<div>
|
||||
<p
|
||||
css={[
|
||||
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>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
|
||||
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 { httpErrorToHuman } from '@/api/http';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
|
@ -11,6 +11,7 @@ import useFlash from '@/plugins/useFlash';
|
|||
import { ServerContext } from '@/state/server';
|
||||
import tw from 'twin.macro';
|
||||
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
||||
import Icon from '@/components/elements/Icon';
|
||||
|
||||
interface Props {
|
||||
schedule: Schedule;
|
||||
|
@ -56,7 +57,7 @@ export default ({ schedule, task }: Props) => {
|
|||
const [ title, icon ] = getActionDetails(task.action);
|
||||
|
||||
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'}/>
|
||||
{isEditing && <TaskDetailsModal
|
||||
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.
|
||||
</ConfirmationModal>
|
||||
<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`}>
|
||||
<p css={tw`md:ml-6 text-neutral-300 uppercase text-xs`}>
|
||||
<div css={tw`flex-none sm:flex-1 w-full sm:w-auto overflow-x-auto`}>
|
||||
<p css={tw`md:ml-6 text-neutral-200 uppercase text-sm`}>
|
||||
{title}
|
||||
</p>
|
||||
{task.payload &&
|
||||
|
@ -87,36 +88,36 @@ export default ({ schedule, task }: Props) => {
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
{task.sequenceId > 1 &&
|
||||
<div css={tw`mr-6`}>
|
||||
<p css={tw`text-center mb-1`}>
|
||||
{task.timeOffset}s
|
||||
</p>
|
||||
<p css={tw`text-neutral-300 uppercase text-2xs`}>
|
||||
Delay Run By
|
||||
</p>
|
||||
<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`flex items-center px-2 py-1 bg-neutral-500 text-sm rounded-full`}>
|
||||
<Icon icon={faClock} css={tw`w-3 h-3 mr-2`}/>
|
||||
{task.timeOffset}s later
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<Can action={'schedule.update'}>
|
||||
<button
|
||||
type={'button'}
|
||||
aria-label={'Edit scheduled task'}
|
||||
css={tw`block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mr-4 ml-auto sm:ml-0`}
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt}/>
|
||||
</button>
|
||||
</Can>
|
||||
<Can action={'schedule.update'}>
|
||||
<button
|
||||
type={'button'}
|
||||
aria-label={'Delete scheduled task'}
|
||||
css={tw`block text-sm p-2 text-neutral-500 hover:text-red-600 transition-colors duration-150`}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt}/>
|
||||
</button>
|
||||
</Can>
|
||||
</div>
|
||||
}
|
||||
<Can action={'schedule.update'}>
|
||||
<button
|
||||
type={'button'}
|
||||
aria-label={'Edit scheduled task'}
|
||||
css={tw`block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mr-4 ml-auto sm:ml-0`}
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt}/>
|
||||
</button>
|
||||
</Can>
|
||||
<Can action={'schedule.update'}>
|
||||
<button
|
||||
type={'button'}
|
||||
aria-label={'Delete scheduled task'}
|
||||
css={tw`block text-sm p-2 text-neutral-500 hover:text-red-600 transition-colors duration-150`}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt}/>
|
||||
</button>
|
||||
</Can>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ export default () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{visible && <EditSubuserModal appear visible onDismissed={() => setVisible(false)}/>}
|
||||
<EditSubuserModal visible={visible} onModalDismissed={() => setVisible(false)}/>
|
||||
<Button onClick={() => setVisible(true)}>
|
||||
<FontAwesomeIcon icon={faUserPlus} css={tw`mr-1`}/> New User
|
||||
</Button>
|
||||
|
|
|
@ -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 { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { array, object, string } from 'yup';
|
||||
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
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 { ServerContext } from '@/state/server';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
@ -17,104 +13,33 @@ import { usePermissions } from '@/plugins/usePermissions';
|
|||
import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Label from '@/components/elements/Label';
|
||||
import Input from '@/components/elements/Input';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import PermissionTitleBox from '@/components/server/users/PermissionTitleBox';
|
||||
import asModal from '@/hoc/asModal';
|
||||
import PermissionRow from '@/components/server/users/PermissionRow';
|
||||
import ModalContext from '@/context/ModalContext';
|
||||
|
||||
type Props = {
|
||||
subuser?: Subuser;
|
||||
} & RequiredModalProps;
|
||||
};
|
||||
|
||||
interface Values {
|
||||
email: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
const PermissionLabel = styled.label`
|
||||
${tw`flex items-center border border-transparent rounded md:p-2 transition-colors duration-75`};
|
||||
text-transform: none;
|
||||
const EditSubuserModal = ({ subuser }: 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 { dismiss, toggleSpinner } = useContext(ModalContext);
|
||||
|
||||
&: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 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 isRootAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
||||
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
|
||||
// that they should not need.
|
||||
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.
|
||||
const editablePermissions = useDeepCompareMemo(() => {
|
||||
|
@ -123,111 +48,25 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
|
|||
|
||||
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.filter(key => loggedInPermissions.indexOf(key) >= 0);
|
||||
}, [ permissions, loggedInPermissions ]);
|
||||
}, [ isRootAdmin, permissions, loggedInPermissions ]);
|
||||
|
||||
return (
|
||||
<Modal {...props} top={false} showSpinnerOverlay={isSubmitting}>
|
||||
<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>) => {
|
||||
const submit = (values: Values) => {
|
||||
toggleSpinner(true);
|
||||
clearFlashes('user:edit');
|
||||
|
||||
createOrUpdateSubuser(uuid, values, subuser)
|
||||
.then(subuser => {
|
||||
appendSubuser(subuser);
|
||||
props.onDismissed();
|
||||
dismiss();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
setSubmitting(false);
|
||||
toggleSpinner(false);
|
||||
clearAndAddHttpError({ key: 'user:edit', error });
|
||||
|
||||
if (ref.current) {
|
||||
|
@ -236,10 +75,8 @@ export default ({ subuser, ...props }: Props) => {
|
|||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearFlashes('user:edit');
|
||||
};
|
||||
useEffect(() => () => {
|
||||
clearFlashes('user:edit');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -258,8 +95,61 @@ export default ({ subuser, ...props }: Props) => {
|
|||
})}
|
||||
>
|
||||
<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>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default asModal<Props>({
|
||||
top: false,
|
||||
})(EditSubuserModal);
|
||||
|
|
65
resources/scripts/components/server/users/PermissionRow.tsx
Normal file
65
resources/scripts/components/server/users/PermissionRow.tsx
Normal 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;
|
|
@ -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;
|
|
@ -19,14 +19,11 @@ export default ({ subuser }: Props) => {
|
|||
|
||||
return (
|
||||
<GreyRowBox css={tw`mb-2`}>
|
||||
{visible &&
|
||||
<EditSubuserModal
|
||||
appear
|
||||
visible
|
||||
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`}>
|
||||
<img css={tw`w-full h-full`} src={`${subuser.image}?s=400`}/>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@ import FlashMessageRender from '@/components/FlashMessageRender';
|
|||
import getServerSubusers from '@/api/server/users/getServerSubusers';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import Can from '@/components/elements/Can';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
export default () => {
|
||||
|
@ -48,7 +48,7 @@ export default () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<PageContentBlock title={'Subusers'}>
|
||||
<ServerContentBlock title={'Subusers'}>
|
||||
<FlashMessageRender byKey={'users'} css={tw`mb-4`}/>
|
||||
{!subusers.length ?
|
||||
<p css={tw`text-center text-sm text-neutral-400`}>
|
||||
|
@ -64,6 +64,6 @@ export default () => {
|
|||
<AddSubuserButton/>
|
||||
</div>
|
||||
</Can>
|
||||
</PageContentBlock>
|
||||
</ServerContentBlock>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import Modal, { ModalProps } from '@/components/elements/Modal';
|
||||
import ModalContext from '@/context/ModalContext';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
export interface AsModalProps {
|
||||
visible: boolean;
|
||||
|
@ -13,7 +12,7 @@ type ExtendedModalProps = Omit<ModalProps, 'appear' | 'visible' | 'onDismissed'>
|
|||
interface State {
|
||||
render: boolean;
|
||||
visible: boolean;
|
||||
modalProps: ExtendedModalProps | undefined;
|
||||
showSpinnerOverlay?: boolean;
|
||||
}
|
||||
|
||||
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 = {
|
||||
render: 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>) {
|
||||
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) {
|
||||
// noinspection JSPotentiallyInvalidUsageOfThis
|
||||
this.setState({ visible: false });
|
||||
|
@ -52,39 +52,32 @@ function asModal<P extends object> (modalProps?: ExtendedModalProps | ((props: P
|
|||
|
||||
dismiss = () => this.setState({ visible: false });
|
||||
|
||||
toggleSpinner = (value?: boolean) => this.setState(s => ({
|
||||
modalProps: {
|
||||
...s.modalProps,
|
||||
showSpinnerOverlay: value || false,
|
||||
},
|
||||
}));
|
||||
toggleSpinner = (value?: boolean) => this.setState({ showSpinnerOverlay: value });
|
||||
|
||||
render () {
|
||||
return (
|
||||
<ModalContext.Provider
|
||||
value={{
|
||||
dismiss: this.dismiss.bind(this),
|
||||
toggleSpinner: this.toggleSpinner.bind(this),
|
||||
}}
|
||||
>
|
||||
{
|
||||
this.state.render ?
|
||||
<Modal
|
||||
appear
|
||||
visible={this.state.visible}
|
||||
onDismissed={() => this.setState({ render: false }, () => {
|
||||
if (typeof this.props.onModalDismissed === 'function') {
|
||||
this.props.onModalDismissed();
|
||||
}
|
||||
})}
|
||||
{...this.state.modalProps}
|
||||
>
|
||||
<Component {...this.props}/>
|
||||
</Modal>
|
||||
:
|
||||
null
|
||||
}
|
||||
</ModalContext.Provider>
|
||||
this.state.render ?
|
||||
<Modal
|
||||
appear
|
||||
visible={this.state.visible}
|
||||
onDismissed={() => this.setState({ render: false }, () => {
|
||||
if (typeof this.props.onModalDismissed === 'function') {
|
||||
this.props.onModalDismissed();
|
||||
}
|
||||
})}
|
||||
{...this.modalProps}
|
||||
>
|
||||
<ModalContext.Provider
|
||||
value={{
|
||||
dismiss: this.dismiss.bind(this),
|
||||
toggleSpinner: this.toggleSpinner.bind(this),
|
||||
}}
|
||||
>
|
||||
<Component {...this.props}/>
|
||||
</ModalContext.Provider>
|
||||
</Modal>
|
||||
:
|
||||
null
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ import NetworkContainer from '@/components/server/network/NetworkContainer';
|
|||
import InstallListener from '@/components/server/InstallListener';
|
||||
import StartupContainer from '@/components/server/startup/StartupContainer';
|
||||
import requireServerPermission from '@/hoc/requireServerPermission';
|
||||
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||
|
||||
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
||||
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.'}
|
||||
/>
|
||||
:
|
||||
<>
|
||||
<ErrorBoundary>
|
||||
<TransitionRouter>
|
||||
<Switch location={location}>
|
||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
||||
|
@ -173,7 +174,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
|||
<Route path={'*'} component={NotFound}/>
|
||||
</Switch>
|
||||
</TransitionRouter>
|
||||
</>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
@endsection
|
||||
|
||||
@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">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li class="active">Mounts</li>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue