forked from Alex/Pterodactyl-Panel
Merge branch 'develop' into feature/server-mounts
This commit is contained in:
commit
29876e023b
@ -8,5 +8,8 @@ indent_size = 4
|
|||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[.*yml]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
@ -2,13 +2,13 @@ APP_ENV=testing
|
|||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_KEY=SomeRandomString3232RandomString
|
APP_KEY=SomeRandomString3232RandomString
|
||||||
APP_THEME=pterodactyl
|
APP_THEME=pterodactyl
|
||||||
APP_TIMEZONE=UTC
|
APP_TIMEZONE=America/Los_Angeles
|
||||||
APP_URL=http://localhost/
|
APP_URL=http://localhost/
|
||||||
|
|
||||||
TESTING_DB_HOST=127.0.0.1
|
TESTING_DB_HOST=127.0.0.1
|
||||||
TESTING_DB_DATABASE=travis
|
TESTING_DB_DATABASE=panel_test
|
||||||
TESTING_DB_USERNAME=root
|
TESTING_DB_USERNAME=root
|
||||||
TESTING_DB_PASSWORD=""
|
TESTING_DB_PASSWORD=
|
||||||
|
|
||||||
CACHE_DRIVER=array
|
CACHE_DRIVER=array
|
||||||
SESSION_DRIVER=array
|
SESSION_DRIVER=array
|
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#github: [DaneEveritt]
|
||||||
|
custom: ["https://paypal.me/PterodactylSoftware"]
|
64
.github/workflows/tests.yml
vendored
Normal file
64
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
jobs:
|
||||||
|
integration_tests:
|
||||||
|
if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:5.7
|
||||||
|
env:
|
||||||
|
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||||
|
MYSQL_DATABASE: panel_test
|
||||||
|
ports:
|
||||||
|
- 3306
|
||||||
|
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
php: [7.3, 7.4]
|
||||||
|
name: PHP ${{ matrix.php }}
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: get cache directory
|
||||||
|
id: composer-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||||
|
- name: cache dependencies
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.php_cs.cache
|
||||||
|
${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-cache-${{ matrix.php }}-${{ hashFiles('**.composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cache-${{ matrix.php }}-
|
||||||
|
- name: setup
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
extensions: cli, openssl, gd, mysql, pdo, mbstring, tokenizer, bcmath, xml, curl, zip
|
||||||
|
tools: composer:v1
|
||||||
|
coverage: none
|
||||||
|
- name: configure
|
||||||
|
run: cp .env.ci .env
|
||||||
|
- name: install dependencies
|
||||||
|
run: composer install --prefer-dist --no-interaction --no-progress
|
||||||
|
- name: run cs-fixer
|
||||||
|
run: vendor/bin/php-cs-fixer fix --dry-run --diff --diff-format=udiff
|
||||||
|
continue-on-error: true
|
||||||
|
- name: execute unit tests
|
||||||
|
run: vendor/bin/phpunit --bootstrap bootstrap/app.php tests/Unit
|
||||||
|
if: ${{ always() }}
|
||||||
|
env:
|
||||||
|
DB_CONNECTION: testing
|
||||||
|
TESTING_DB_HOST: UNIT_NO_DB
|
||||||
|
- name: execute integration tests
|
||||||
|
run: vendor/bin/phpunit tests/Integration
|
||||||
|
if: ${{ always() }}
|
||||||
|
env:
|
||||||
|
TESTING_DB_PORT: ${{ job.services.mysql.ports[3306] }}
|
||||||
|
TESTING_DB_USERNAME: root
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -32,3 +32,4 @@ coverage.xml
|
|||||||
resources/lang/locales.js
|
resources/lang/locales.js
|
||||||
resources/assets/pterodactyl/scripts/helpers/ziggy.js
|
resources/assets/pterodactyl/scripts/helpers/ziggy.js
|
||||||
resources/assets/scripts/helpers/ziggy.js
|
resources/assets/scripts/helpers/ziggy.js
|
||||||
|
.phpunit.result.cache
|
||||||
|
@ -38,7 +38,7 @@ class CleanOrphanedApiKeysCommand extends Command
|
|||||||
/**
|
/**
|
||||||
* Delete all orphaned API keys from the database when upgrading from 0.6 to 0.7.
|
* Delete all orphaned API keys from the database when upgrading from 0.6 to 0.7.
|
||||||
*
|
*
|
||||||
* @return null|void
|
* @return void|null
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
|
@ -19,7 +19,7 @@ interface SessionRepositoryInterface extends RepositoryInterface
|
|||||||
*
|
*
|
||||||
* @param int $user
|
* @param int $user
|
||||||
* @param string $session
|
* @param string $session
|
||||||
* @return null|int
|
* @return int|null
|
||||||
*/
|
*/
|
||||||
public function deleteUserSession(int $user, string $session);
|
public function deleteUserSession(int $user, string $session);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ interface TaskRepositoryInterface extends RepositoryInterface
|
|||||||
*
|
*
|
||||||
* @param int $schedule
|
* @param int $schedule
|
||||||
* @param int $index
|
* @param int $index
|
||||||
* @return null|\Pterodactyl\Models\Task
|
* @return \Pterodactyl\Models\Task|null
|
||||||
*/
|
*/
|
||||||
public function getNextTask(int $schedule, int $index);
|
public function getNextTask(int $schedule, int $index);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
namespace Pterodactyl\Exceptions;
|
namespace Pterodactyl\Exceptions;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Throwable;
|
||||||
use PDOException;
|
use PDOException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Swift_TransportException;
|
use Swift_TransportException;
|
||||||
@ -72,12 +73,12 @@ class Handler extends ExceptionHandler
|
|||||||
* services such as AWS Cloudwatch or other monitoring you can replace the
|
* services such as AWS Cloudwatch or other monitoring you can replace the
|
||||||
* contents of this function with a call to the parent reporter.
|
* contents of this function with a call to the parent reporter.
|
||||||
*
|
*
|
||||||
* @param \Exception $exception
|
* @param \Throwable $exception
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*
|
*
|
||||||
* @throws \Exception
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function report(Exception $exception)
|
public function report(Throwable $exception)
|
||||||
{
|
{
|
||||||
if (! config('app.exceptions.report_all', false) && $this->shouldntReport($exception)) {
|
if (! config('app.exceptions.report_all', false) && $this->shouldntReport($exception)) {
|
||||||
return null;
|
return null;
|
||||||
@ -103,7 +104,7 @@ class Handler extends ExceptionHandler
|
|||||||
return $logger->error($exception);
|
return $logger->error($exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateCleanedExceptionStack(Exception $exception)
|
private function generateCleanedExceptionStack(Throwable $exception)
|
||||||
{
|
{
|
||||||
$cleanedStack = '';
|
$cleanedStack = '';
|
||||||
foreach ($exception->getTrace() as $index => $item) {
|
foreach ($exception->getTrace() as $index => $item) {
|
||||||
@ -133,12 +134,12 @@ class Handler extends ExceptionHandler
|
|||||||
* Render an exception into an HTTP response.
|
* Render an exception into an HTTP response.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @param \Exception $exception
|
* @param \Throwable $exception
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
*
|
*
|
||||||
* @throws \Exception
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function render($request, Exception $exception)
|
public function render($request, Throwable $exception)
|
||||||
{
|
{
|
||||||
$connections = Container::getInstance()->make(Connection::class);
|
$connections = Container::getInstance()->make(Connection::class);
|
||||||
|
|
||||||
@ -200,11 +201,11 @@ class Handler extends ExceptionHandler
|
|||||||
/**
|
/**
|
||||||
* Return the exception as a JSONAPI representation for use on API requests.
|
* Return the exception as a JSONAPI representation for use on API requests.
|
||||||
*
|
*
|
||||||
* @param \Exception $exception
|
* @param \Throwable $exception
|
||||||
* @param array $override
|
* @param array $override
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function convertToArray(Exception $exception, array $override = []): array
|
public static function convertToArray(Throwable $exception, array $override = []): array
|
||||||
{
|
{
|
||||||
$error = [
|
$error = [
|
||||||
'code' => class_basename($exception),
|
'code' => class_basename($exception),
|
||||||
@ -259,10 +260,10 @@ class Handler extends ExceptionHandler
|
|||||||
* Converts an exception into an array to render in the response. Overrides
|
* Converts an exception into an array to render in the response. Overrides
|
||||||
* Laravel's built-in converter to output as a JSONAPI spec compliant object.
|
* Laravel's built-in converter to output as a JSONAPI spec compliant object.
|
||||||
*
|
*
|
||||||
* @param \Exception $exception
|
* @param \Throwable $exception
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function convertExceptionToArray(Exception $exception)
|
protected function convertExceptionToArray(Throwable $exception)
|
||||||
{
|
{
|
||||||
return self::convertToArray($exception);
|
return self::convertToArray($exception);
|
||||||
}
|
}
|
||||||
|
21
app/Exceptions/Service/ServiceLimitExceededException.php
Normal file
21
app/Exceptions/Service/ServiceLimitExceededException.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Exceptions\Service;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
use Pterodactyl\Exceptions\DisplayException;
|
||||||
|
|
||||||
|
class ServiceLimitExceededException extends DisplayException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Exception thrown when something goes over a defined limit, such as allocated
|
||||||
|
* ports, tasks, databases, etc.
|
||||||
|
*
|
||||||
|
* @param string $message
|
||||||
|
* @param \Throwable|null $previous
|
||||||
|
*/
|
||||||
|
public function __construct(string $message, Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $previous, self::LEVEL_WARNING);
|
||||||
|
}
|
||||||
|
}
|
@ -17,10 +17,12 @@ use Prologue\Alerts\AlertsMessageBag;
|
|||||||
use GuzzleHttp\Exception\RequestException;
|
use GuzzleHttp\Exception\RequestException;
|
||||||
use Pterodactyl\Exceptions\DisplayException;
|
use Pterodactyl\Exceptions\DisplayException;
|
||||||
use Pterodactyl\Http\Controllers\Controller;
|
use Pterodactyl\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Pterodactyl\Services\Servers\SuspensionService;
|
use Pterodactyl\Services\Servers\SuspensionService;
|
||||||
use Pterodactyl\Repositories\Eloquent\MountRepository;
|
use Pterodactyl\Repositories\Eloquent\MountRepository;
|
||||||
use Pterodactyl\Services\Servers\ServerDeletionService;
|
use Pterodactyl\Services\Servers\ServerDeletionService;
|
||||||
use Pterodactyl\Services\Servers\ReinstallServerService;
|
use Pterodactyl\Services\Servers\ReinstallServerService;
|
||||||
|
use Pterodactyl\Exceptions\Model\DataValidationException;
|
||||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||||
use Pterodactyl\Services\Servers\BuildModificationService;
|
use Pterodactyl\Services\Servers\BuildModificationService;
|
||||||
use Pterodactyl\Services\Databases\DatabasePasswordService;
|
use Pterodactyl\Services\Databases\DatabasePasswordService;
|
||||||
@ -284,16 +286,21 @@ class ServersController extends Controller
|
|||||||
* @return \Illuminate\Http\RedirectResponse
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
*
|
*
|
||||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
*/
|
*/
|
||||||
public function updateBuild(Request $request, Server $server)
|
public function updateBuild(Request $request, Server $server)
|
||||||
{
|
{
|
||||||
$this->buildModificationService->handle($server, $request->only([
|
try {
|
||||||
'allocation_id', 'add_allocations', 'remove_allocations',
|
$this->buildModificationService->handle($server, $request->only([
|
||||||
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
|
'allocation_id', 'add_allocations', 'remove_allocations',
|
||||||
'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled',
|
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
|
||||||
]));
|
'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled',
|
||||||
|
]));
|
||||||
|
} catch (DataValidationException $exception) {
|
||||||
|
throw new ValidationException($exception->validator);
|
||||||
|
}
|
||||||
|
|
||||||
$this->alert->success(trans('admin/server.alerts.build_updated'))->flash();
|
$this->alert->success(trans('admin/server.alerts.build_updated'))->flash();
|
||||||
|
|
||||||
return redirect()->route('admin.servers.view.build', $server->id);
|
return redirect()->route('admin.servers.view.build', $server->id);
|
||||||
@ -341,12 +348,12 @@ class ServersController extends Controller
|
|||||||
* Creates a new database assigned to a specific server.
|
* Creates a new database assigned to a specific server.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Admin\Servers\Databases\StoreServerDatabaseRequest $request
|
* @param \Pterodactyl\Http\Requests\Admin\Servers\Databases\StoreServerDatabaseRequest $request
|
||||||
* @param int $server
|
* @param \Pterodactyl\Models\Server $server
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
*
|
*
|
||||||
* @throws \Exception
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function newDatabase(StoreServerDatabaseRequest $request, $server)
|
public function newDatabase(StoreServerDatabaseRequest $request, Server $server)
|
||||||
{
|
{
|
||||||
$this->databaseManagementService->create($server, [
|
$this->databaseManagementService->create($server, [
|
||||||
'database' => $request->input('database'),
|
'database' => $request->input('database'),
|
||||||
@ -355,7 +362,7 @@ class ServersController extends Controller
|
|||||||
'max_connections' => $request->input('max_connections'),
|
'max_connections' => $request->input('max_connections'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect()->route('admin.servers.view.database', $server)->withInput();
|
return redirect()->route('admin.servers.view.database', $server->id)->withInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,13 +57,12 @@ class DatabaseController extends ApplicationApiController
|
|||||||
* server.
|
* server.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request
|
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request
|
||||||
|
* @param \Pterodactyl\Models\Server $server
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function index(GetServerDatabasesRequest $request): array
|
public function index(GetServerDatabasesRequest $request, Server $server): array
|
||||||
{
|
{
|
||||||
$databases = $this->repository->getDatabasesForServer($request->getModel(Server::class)->id);
|
return $this->fractal->collection($server->databases)
|
||||||
|
|
||||||
return $this->fractal->collection($databases)
|
|
||||||
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
|
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
@ -72,11 +71,13 @@ class DatabaseController extends ApplicationApiController
|
|||||||
* Return a single server database.
|
* Return a single server database.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request
|
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request
|
||||||
|
* @param \Pterodactyl\Models\Server $server
|
||||||
|
* @param \Pterodactyl\Models\Database $database
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function view(GetServerDatabaseRequest $request): array
|
public function view(GetServerDatabaseRequest $request, Server $server, Database $database): array
|
||||||
{
|
{
|
||||||
return $this->fractal->item($request->getModel(Database::class))
|
return $this->fractal->item($database)
|
||||||
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
|
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
@ -85,29 +86,31 @@ class DatabaseController extends ApplicationApiController
|
|||||||
* Reset the password for a specific server database.
|
* Reset the password for a specific server database.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request
|
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request
|
||||||
* @return \Illuminate\Http\Response
|
* @param \Pterodactyl\Models\Server $server
|
||||||
|
* @param \Pterodactyl\Models\Database $database
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*
|
*
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function resetPassword(ServerDatabaseWriteRequest $request): Response
|
public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): JsonResponse
|
||||||
{
|
{
|
||||||
$this->databasePasswordService->handle($request->getModel(Database::class));
|
$this->databasePasswordService->handle($database);
|
||||||
|
|
||||||
return response('', 204);
|
return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new database on the Panel for a given server.
|
* Create a new database on the Panel for a given server.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request
|
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request
|
||||||
|
* @param \Pterodactyl\Models\Server $server
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*
|
*
|
||||||
* @throws \Exception
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function store(StoreServerDatabaseRequest $request): JsonResponse
|
public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse
|
||||||
{
|
{
|
||||||
$server = $request->getModel(Server::class);
|
$database = $this->databaseManagementService->create($server, $request->validated());
|
||||||
$database = $this->databaseManagementService->create($server->id, $request->validated());
|
|
||||||
|
|
||||||
return $this->fractal->item($database)
|
return $this->fractal->item($database)
|
||||||
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
|
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
|
||||||
@ -117,7 +120,7 @@ class DatabaseController extends ApplicationApiController
|
|||||||
'database' => $database->id,
|
'database' => $database->id,
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->respond(201);
|
->respond(Response::HTTP_CREATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,39 +100,20 @@ class UserController extends ApplicationApiController
|
|||||||
* meta. If there are no errors this is an empty array.
|
* meta. If there are no errors this is an empty array.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Application\Users\UpdateUserRequest $request
|
* @param \Pterodactyl\Http\Requests\Api\Application\Users\UpdateUserRequest $request
|
||||||
|
* @param \Pterodactyl\Models\User $user
|
||||||
* @return array
|
* @return array
|
||||||
*
|
*
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function update(UpdateUserRequest $request): array
|
public function update(UpdateUserRequest $request, User $user): array
|
||||||
{
|
{
|
||||||
$this->updateService->setUserLevel(User::USER_LEVEL_ADMIN);
|
$this->updateService->setUserLevel(User::USER_LEVEL_ADMIN);
|
||||||
$collection = $this->updateService->handle($request->getModel(User::class), $request->validated());
|
$user = $this->updateService->handle($user, $request->validated());
|
||||||
|
|
||||||
$errors = [];
|
$response = $this->fractal->item($user)
|
||||||
if (! empty($collection->get('exceptions'))) {
|
|
||||||
foreach ($collection->get('exceptions') as $node => $exception) {
|
|
||||||
/** @var \GuzzleHttp\Exception\RequestException $exception */
|
|
||||||
/** @var \GuzzleHttp\Psr7\Response|null $response */
|
|
||||||
$response = method_exists($exception, 'getResponse') ? $exception->getResponse() : null;
|
|
||||||
$message = trans('admin/server.exceptions.daemon_exception', [
|
|
||||||
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$errors[] = ['message' => $message, 'node' => $node];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = $this->fractal->item($collection->get('model'))
|
|
||||||
->transformWith($this->getTransformer(UserTransformer::class));
|
->transformWith($this->getTransformer(UserTransformer::class));
|
||||||
|
|
||||||
if (count($errors) > 0) {
|
|
||||||
$response->addMeta([
|
|
||||||
'revocation_errors' => $errors,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $response->toArray();
|
return $response->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,16 +52,16 @@ class AccountController extends ClientApiController
|
|||||||
* Update the authenticated user's email address.
|
* Update the authenticated user's email address.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest $request
|
* @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest $request
|
||||||
* @return \Illuminate\Http\Response
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*
|
*
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function updateEmail(UpdateEmailRequest $request): Response
|
public function updateEmail(UpdateEmailRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$this->updateService->handle($request->user(), $request->validated());
|
$this->updateService->handle($request->user(), $request->validated());
|
||||||
|
|
||||||
return response('', Response::HTTP_CREATED);
|
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,12 +74,12 @@ class AccountController extends ClientApiController
|
|||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function updatePassword(UpdatePasswordRequest $request): \Illuminate\Http\JsonResponse
|
public function updatePassword(UpdatePasswordRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$this->updateService->handle($request->user(), $request->validated());
|
$this->updateService->handle($request->user(), $request->validated());
|
||||||
|
|
||||||
$this->sessionGuard->logoutOtherDevices($request->input('password'));
|
$this->sessionGuard->logoutOtherDevices($request->input('password'));
|
||||||
|
|
||||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,6 +103,7 @@ class ApiKeyController extends ClientApiController
|
|||||||
public function delete(ClientApiRequest $request, string $identifier)
|
public function delete(ClientApiRequest $request, string $identifier)
|
||||||
{
|
{
|
||||||
$response = $this->repository->deleteWhere([
|
$response = $this->repository->deleteWhere([
|
||||||
|
'key_type' => ApiKey::TYPE_ACCOUNT,
|
||||||
'user_id' => $request->user()->id,
|
'user_id' => $request->user()->id,
|
||||||
'identifier' => $identifier,
|
'identifier' => $identifier,
|
||||||
]);
|
]);
|
||||||
|
@ -69,9 +69,7 @@ class DatabaseController extends ClientApiController
|
|||||||
*/
|
*/
|
||||||
public function index(GetDatabasesRequest $request, Server $server): array
|
public function index(GetDatabasesRequest $request, Server $server): array
|
||||||
{
|
{
|
||||||
$databases = $this->repository->getDatabasesForServer($server->id);
|
return $this->fractal->collection($server->databases)
|
||||||
|
|
||||||
return $this->fractal->collection($databases)
|
|
||||||
->transformWith($this->getTransformer(DatabaseTransformer::class))
|
->transformWith($this->getTransformer(DatabaseTransformer::class))
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
@ -83,6 +81,8 @@ class DatabaseController extends ClientApiController
|
|||||||
* @param \Pterodactyl\Models\Server $server
|
* @param \Pterodactyl\Models\Server $server
|
||||||
* @return array
|
* @return array
|
||||||
*
|
*
|
||||||
|
* @throws \Throwable
|
||||||
|
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
|
||||||
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
||||||
*/
|
*/
|
||||||
public function store(StoreDatabaseRequest $request, Server $server): array
|
public function store(StoreDatabaseRequest $request, Server $server): array
|
||||||
|
@ -147,7 +147,7 @@ class ScheduleController extends ClientApiController
|
|||||||
{
|
{
|
||||||
$this->repository->delete($schedule->id);
|
$this->repository->delete($schedule->id);
|
||||||
|
|
||||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,6 +13,7 @@ use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
|||||||
use Pterodactyl\Transformers\Api\Client\TaskTransformer;
|
use Pterodactyl\Transformers\Api\Client\TaskTransformer;
|
||||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||||
|
use Pterodactyl\Exceptions\Service\ServiceLimitExceededException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest;
|
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest;
|
||||||
|
|
||||||
@ -44,11 +45,15 @@ class ScheduleTaskController extends ClientApiController
|
|||||||
* @return array
|
* @return array
|
||||||
*
|
*
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Service\ServiceLimitExceededException
|
||||||
*/
|
*/
|
||||||
public function store(StoreTaskRequest $request, Server $server, Schedule $schedule)
|
public function store(StoreTaskRequest $request, Server $server, Schedule $schedule)
|
||||||
{
|
{
|
||||||
if ($schedule->server_id !== $server->id) {
|
$limit = config('pterodactyl.client_features.schedules.per_schedule_task_limit', 10);
|
||||||
throw new NotFoundHttpException;
|
if ($schedule->tasks()->count() >= $limit) {
|
||||||
|
throw new ServiceLimitExceededException(
|
||||||
|
"Schedules may not have more than {$limit} tasks associated with them. Creating this task would put this schedule over the limit."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$lastTask = $schedule->tasks->last();
|
$lastTask = $schedule->tasks->last();
|
||||||
@ -58,7 +63,7 @@ class ScheduleTaskController extends ClientApiController
|
|||||||
'schedule_id' => $schedule->id,
|
'schedule_id' => $schedule->id,
|
||||||
'sequence_id' => ($lastTask->sequence_id ?? 0) + 1,
|
'sequence_id' => ($lastTask->sequence_id ?? 0) + 1,
|
||||||
'action' => $request->input('action'),
|
'action' => $request->input('action'),
|
||||||
'payload' => $request->input('payload'),
|
'payload' => $request->input('payload') ?? '',
|
||||||
'time_offset' => $request->input('time_offset'),
|
'time_offset' => $request->input('time_offset'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -87,7 +92,7 @@ class ScheduleTaskController extends ClientApiController
|
|||||||
|
|
||||||
$this->repository->update($task->id, [
|
$this->repository->update($task->id, [
|
||||||
'action' => $request->input('action'),
|
'action' => $request->input('action'),
|
||||||
'payload' => $request->input('payload'),
|
'payload' => $request->input('payload') ?? '',
|
||||||
'time_offset' => $request->input('time_offset'),
|
'time_offset' => $request->input('time_offset'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class SettingsController extends ClientApiController
|
|||||||
'name' => $request->input('name'),
|
'name' => $request->input('name'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,6 +71,6 @@ class SettingsController extends ClientApiController
|
|||||||
{
|
{
|
||||||
$this->reinstallServerService->reinstall($server);
|
$this->reinstallServerService->reinstall($server);
|
||||||
|
|
||||||
return JsonResponse::create([], Response::HTTP_ACCEPTED);
|
return new JsonResponse([], Response::HTTP_ACCEPTED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ use Illuminate\Http\Response;
|
|||||||
use Pterodactyl\Models\Server;
|
use Pterodactyl\Models\Server;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Pterodactyl\Models\Permission;
|
use Pterodactyl\Models\Permission;
|
||||||
use Illuminate\Contracts\Cache\Repository;
|
|
||||||
use Pterodactyl\Services\Nodes\NodeJWTService;
|
use Pterodactyl\Services\Nodes\NodeJWTService;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
@ -16,11 +15,6 @@ use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
|||||||
|
|
||||||
class WebsocketController extends ClientApiController
|
class WebsocketController extends ClientApiController
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var \Illuminate\Contracts\Cache\Repository
|
|
||||||
*/
|
|
||||||
private $cache;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Pterodactyl\Services\Nodes\NodeJWTService
|
* @var \Pterodactyl\Services\Nodes\NodeJWTService
|
||||||
*/
|
*/
|
||||||
@ -36,16 +30,13 @@ class WebsocketController extends ClientApiController
|
|||||||
*
|
*
|
||||||
* @param \Pterodactyl\Services\Nodes\NodeJWTService $jwtService
|
* @param \Pterodactyl\Services\Nodes\NodeJWTService $jwtService
|
||||||
* @param \Pterodactyl\Services\Servers\GetUserPermissionsService $permissionsService
|
* @param \Pterodactyl\Services\Servers\GetUserPermissionsService $permissionsService
|
||||||
* @param \Illuminate\Contracts\Cache\Repository $cache
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
NodeJWTService $jwtService,
|
NodeJWTService $jwtService,
|
||||||
GetUserPermissionsService $permissionsService,
|
GetUserPermissionsService $permissionsService
|
||||||
Repository $cache
|
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->cache = $cache;
|
|
||||||
$this->jwtService = $jwtService;
|
$this->jwtService = $jwtService;
|
||||||
$this->permissionsService = $permissionsService;
|
$this->permissionsService = $permissionsService;
|
||||||
}
|
}
|
||||||
@ -78,7 +69,7 @@ class WebsocketController extends ClientApiController
|
|||||||
|
|
||||||
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress());
|
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress());
|
||||||
|
|
||||||
return JsonResponse::create([
|
return new JsonResponse([
|
||||||
'data' => [
|
'data' => [
|
||||||
'token' => $token->__toString(),
|
'token' => $token->__toString(),
|
||||||
'socket' => $socket . sprintf('/api/servers/%s/ws', $server->uuid),
|
'socket' => $socket . sprintf('/api/servers/%s/ws', $server->uuid),
|
||||||
|
@ -61,11 +61,11 @@ class TwoFactorController extends ClientApiController
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
if ($request->user()->totp_enabled) {
|
if ($request->user()->use_totp) {
|
||||||
throw new BadRequestHttpException('Two-factor authentication is already enabled on this account.');
|
throw new BadRequestHttpException('Two-factor authentication is already enabled on this account.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse::create([
|
return new JsonResponse([
|
||||||
'data' => [
|
'data' => [
|
||||||
'image_url_data' => $this->setupService->handle($request->user()),
|
'image_url_data' => $this->setupService->handle($request->user()),
|
||||||
],
|
],
|
||||||
@ -96,9 +96,14 @@ class TwoFactorController extends ClientApiController
|
|||||||
throw new ValidationException($validator);
|
throw new ValidationException($validator);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true);
|
$tokens = $this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true);
|
||||||
|
|
||||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
return new JsonResponse([
|
||||||
|
'object' => 'recovery_tokens',
|
||||||
|
'attributes' => [
|
||||||
|
'tokens' => $tokens,
|
||||||
|
],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,6 +129,6 @@ class TwoFactorController extends ClientApiController
|
|||||||
'use_totp' => false,
|
'use_totp' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,10 +68,11 @@ abstract class AbstractLoginController extends Controller
|
|||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
|
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
|
||||||
|
* @param string|null $message
|
||||||
*
|
*
|
||||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||||
*/
|
*/
|
||||||
protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null)
|
protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null, string $message = null)
|
||||||
{
|
{
|
||||||
$this->incrementLoginAttempts($request);
|
$this->incrementLoginAttempts($request);
|
||||||
$this->fireFailedLoginEvent($user, [
|
$this->fireFailedLoginEvent($user, [
|
||||||
@ -79,7 +80,9 @@ abstract class AbstractLoginController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->route()->named('auth.login-checkpoint')) {
|
if ($request->route()->named('auth.login-checkpoint')) {
|
||||||
throw new DisplayException(trans('auth.two_factor.checkpoint_failed'));
|
throw new DisplayException(
|
||||||
|
$message ?? trans('auth.two_factor.checkpoint_failed')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new DisplayException(trans('auth.failed'));
|
throw new DisplayException(trans('auth.failed'));
|
||||||
@ -116,7 +119,7 @@ abstract class AbstractLoginController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function getField(string $input = null): string
|
protected function getField(string $input = null): string
|
||||||
{
|
{
|
||||||
return str_contains($input, '@') ? 'email' : 'username';
|
return ($input && str_contains($input, '@')) ? 'email' : 'username';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,6 +11,7 @@ use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest;
|
|||||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||||
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
|
||||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||||
|
use Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository;
|
||||||
|
|
||||||
class LoginCheckpointController extends AbstractLoginController
|
class LoginCheckpointController extends AbstractLoginController
|
||||||
{
|
{
|
||||||
@ -34,6 +35,11 @@ class LoginCheckpointController extends AbstractLoginController
|
|||||||
*/
|
*/
|
||||||
private $encrypter;
|
private $encrypter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository
|
||||||
|
*/
|
||||||
|
private $recoveryTokenRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LoginCheckpointController constructor.
|
* LoginCheckpointController constructor.
|
||||||
*
|
*
|
||||||
@ -42,6 +48,7 @@ class LoginCheckpointController extends AbstractLoginController
|
|||||||
* @param \PragmaRX\Google2FA\Google2FA $google2FA
|
* @param \PragmaRX\Google2FA\Google2FA $google2FA
|
||||||
* @param \Illuminate\Contracts\Config\Repository $config
|
* @param \Illuminate\Contracts\Config\Repository $config
|
||||||
* @param \Illuminate\Contracts\Cache\Repository $cache
|
* @param \Illuminate\Contracts\Cache\Repository $cache
|
||||||
|
* @param \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository $recoveryTokenRepository
|
||||||
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
|
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@ -50,6 +57,7 @@ class LoginCheckpointController extends AbstractLoginController
|
|||||||
Google2FA $google2FA,
|
Google2FA $google2FA,
|
||||||
Repository $config,
|
Repository $config,
|
||||||
CacheRepository $cache,
|
CacheRepository $cache,
|
||||||
|
RecoveryTokenRepository $recoveryTokenRepository,
|
||||||
UserRepositoryInterface $repository
|
UserRepositoryInterface $repository
|
||||||
) {
|
) {
|
||||||
parent::__construct($auth, $config);
|
parent::__construct($auth, $config);
|
||||||
@ -58,6 +66,7 @@ class LoginCheckpointController extends AbstractLoginController
|
|||||||
$this->cache = $cache;
|
$this->cache = $cache;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
$this->encrypter = $encrypter;
|
$this->encrypter = $encrypter;
|
||||||
|
$this->recoveryTokenRepository = $recoveryTokenRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,21 +85,35 @@ class LoginCheckpointController extends AbstractLoginController
|
|||||||
public function __invoke(LoginCheckpointRequest $request): JsonResponse
|
public function __invoke(LoginCheckpointRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$token = $request->input('confirmation_token');
|
$token = $request->input('confirmation_token');
|
||||||
|
$recoveryToken = $request->input('recovery_token');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
/** @var \Pterodactyl\Models\User $user */
|
||||||
$user = $this->repository->find($this->cache->get($token, 0));
|
$user = $this->repository->find($this->cache->get($token, 0));
|
||||||
} catch (RecordNotFoundException $exception) {
|
} catch (RecordNotFoundException $exception) {
|
||||||
return $this->sendFailedLoginResponse($request);
|
return $this->sendFailedLoginResponse($request, null, 'The authentication token provided has expired, please refresh the page and try again.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$decrypted = $this->encrypter->decrypt($user->totp_secret);
|
// 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);
|
||||||
|
|
||||||
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
|
return $this->sendLoginResponse($user, $request);
|
||||||
$this->cache->delete($token);
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$decrypted = $this->encrypter->decrypt($user->totp_secret);
|
||||||
|
|
||||||
return $this->sendLoginResponse($user, $request);
|
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
|
||||||
|
$this->cache->delete($token);
|
||||||
|
|
||||||
|
return $this->sendLoginResponse($user, $request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->sendFailedLoginResponse($request, $user);
|
return $this->sendFailedLoginResponse($request, $user, ! empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ class LoginController extends AbstractLoginController
|
|||||||
$token = Str::random(64);
|
$token = Str::random(64);
|
||||||
$this->cache->put($token, $user->id, Chronos::now()->addMinutes(5));
|
$this->cache->put($token, $user->id, Chronos::now()->addMinutes(5));
|
||||||
|
|
||||||
return JsonResponse::create([
|
return new JsonResponse([
|
||||||
'data' => [
|
'data' => [
|
||||||
'complete' => false,
|
'complete' => false,
|
||||||
'confirmation_token' => $token,
|
'confirmation_token' => $token,
|
||||||
|
@ -9,6 +9,7 @@ use Pterodactyl\Http\Middleware\TrimStrings;
|
|||||||
use Pterodactyl\Http\Middleware\TrustProxies;
|
use Pterodactyl\Http\Middleware\TrustProxies;
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use Pterodactyl\Http\Middleware\EncryptCookies;
|
use Pterodactyl\Http\Middleware\EncryptCookies;
|
||||||
|
use Pterodactyl\Http\Middleware\Api\IsValidJson;
|
||||||
use Pterodactyl\Http\Middleware\VerifyCsrfToken;
|
use Pterodactyl\Http\Middleware\VerifyCsrfToken;
|
||||||
use Pterodactyl\Http\Middleware\VerifyReCaptcha;
|
use Pterodactyl\Http\Middleware\VerifyReCaptcha;
|
||||||
use Pterodactyl\Http\Middleware\AdminAuthenticate;
|
use Pterodactyl\Http\Middleware\AdminAuthenticate;
|
||||||
@ -28,12 +29,8 @@ use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
|
|||||||
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||||
use Pterodactyl\Http\Middleware\Server\AccessingValidServer;
|
use Pterodactyl\Http\Middleware\Server\AccessingValidServer;
|
||||||
use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser;
|
|
||||||
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
|
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
|
||||||
use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer;
|
|
||||||
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
||||||
use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer;
|
|
||||||
use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer;
|
|
||||||
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
|
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
|
||||||
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
||||||
use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings;
|
use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings;
|
||||||
@ -72,7 +69,7 @@ class Kernel extends HttpKernel
|
|||||||
RequireTwoFactorAuthentication::class,
|
RequireTwoFactorAuthentication::class,
|
||||||
],
|
],
|
||||||
'api' => [
|
'api' => [
|
||||||
'throttle:240,1',
|
IsValidJson::class,
|
||||||
ApiSubstituteBindings::class,
|
ApiSubstituteBindings::class,
|
||||||
SetSessionDriver::class,
|
SetSessionDriver::class,
|
||||||
'api..key:' . ApiKey::TYPE_APPLICATION,
|
'api..key:' . ApiKey::TYPE_APPLICATION,
|
||||||
@ -80,10 +77,10 @@ class Kernel extends HttpKernel
|
|||||||
AuthenticateIPAccess::class,
|
AuthenticateIPAccess::class,
|
||||||
],
|
],
|
||||||
'client-api' => [
|
'client-api' => [
|
||||||
'throttle:240,1',
|
|
||||||
StartSession::class,
|
StartSession::class,
|
||||||
SetSessionDriver::class,
|
SetSessionDriver::class,
|
||||||
AuthenticateSession::class,
|
AuthenticateSession::class,
|
||||||
|
IsValidJson::class,
|
||||||
SubstituteClientApiBindings::class,
|
SubstituteClientApiBindings::class,
|
||||||
'api..key:' . ApiKey::TYPE_ACCOUNT,
|
'api..key:' . ApiKey::TYPE_ACCOUNT,
|
||||||
AuthenticateIPAccess::class,
|
AuthenticateIPAccess::class,
|
||||||
@ -104,7 +101,6 @@ class Kernel extends HttpKernel
|
|||||||
'auth.basic' => AuthenticateWithBasicAuth::class,
|
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||||
'guest' => RedirectIfAuthenticated::class,
|
'guest' => RedirectIfAuthenticated::class,
|
||||||
'server' => AccessingValidServer::class,
|
'server' => AccessingValidServer::class,
|
||||||
'subuser.auth' => AuthenticateAsSubuser::class,
|
|
||||||
'admin' => AdminAuthenticate::class,
|
'admin' => AdminAuthenticate::class,
|
||||||
'csrf' => VerifyCsrfToken::class,
|
'csrf' => VerifyCsrfToken::class,
|
||||||
'throttle' => ThrottleRequests::class,
|
'throttle' => ThrottleRequests::class,
|
||||||
@ -113,14 +109,6 @@ class Kernel extends HttpKernel
|
|||||||
'recaptcha' => VerifyReCaptcha::class,
|
'recaptcha' => VerifyReCaptcha::class,
|
||||||
'node.maintenance' => MaintenanceMiddleware::class,
|
'node.maintenance' => MaintenanceMiddleware::class,
|
||||||
|
|
||||||
// Server specific middleware (used for authenticating access to resources)
|
|
||||||
//
|
|
||||||
// These are only used for individual server authentication, and not global
|
|
||||||
// actions from other resources. They are defined in the route files.
|
|
||||||
'server..database' => DatabaseBelongsToServer::class,
|
|
||||||
'server..subuser' => SubuserBelongsToServer::class,
|
|
||||||
'server..schedule' => ScheduleBelongsToServer::class,
|
|
||||||
|
|
||||||
// API Specific Middleware
|
// API Specific Middleware
|
||||||
'api..key' => AuthenticateKey::class,
|
'api..key' => AuthenticateKey::class,
|
||||||
];
|
];
|
||||||
|
@ -65,7 +65,7 @@ class AuthenticateServerAccess
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($server->suspended) {
|
if ($server->suspended) {
|
||||||
throw new AccessDeniedHttpException('Cannot access a server that is marked as being suspended.');
|
throw new AccessDeniedHttpException('This server is currenty suspended and the functionality requested is unavailable.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $server->isInstalled()) {
|
if (! $server->isInstalled()) {
|
||||||
|
@ -5,8 +5,8 @@ namespace Pterodactyl\Http\Middleware\Api\Daemon;
|
|||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Contracts\Encryption\Encrypter;
|
use Illuminate\Contracts\Encryption\Encrypter;
|
||||||
|
use Pterodactyl\Repositories\Eloquent\NodeRepository;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
|
||||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
@ -14,10 +14,15 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|||||||
class DaemonAuthenticate
|
class DaemonAuthenticate
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
|
||||||
*/
|
*/
|
||||||
private $repository;
|
private $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Illuminate\Contracts\Encryption\Encrypter
|
||||||
|
*/
|
||||||
|
private $encrypter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Daemon routes that this middleware should be skipped on.
|
* Daemon routes that this middleware should be skipped on.
|
||||||
*
|
*
|
||||||
@ -27,18 +32,13 @@ class DaemonAuthenticate
|
|||||||
'daemon.configuration',
|
'daemon.configuration',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Illuminate\Contracts\Encryption\Encrypter
|
|
||||||
*/
|
|
||||||
private $encrypter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DaemonAuthenticate constructor.
|
* DaemonAuthenticate constructor.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
||||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
* @param \Pterodactyl\Repositories\Eloquent\NodeRepository $repository
|
||||||
*/
|
*/
|
||||||
public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository)
|
public function __construct(Encrypter $encrypter, NodeRepository $repository)
|
||||||
{
|
{
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
$this->encrypter = $encrypter;
|
$this->encrypter = $encrypter;
|
||||||
|
38
app/Http/Middleware/Api/IsValidJson.php
Normal file
38
app/Http/Middleware/Api/IsValidJson.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Middleware\Api;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
|
||||||
|
class IsValidJson
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Throw an exception if the request should be valid JSON data but there is an error while
|
||||||
|
* parsing the data. This avoids confusing validation errors where every field is flagged and
|
||||||
|
* it is not immediately clear that there is an issue with the JSON being passed.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next)
|
||||||
|
{
|
||||||
|
if ($request->isJson() && ! empty($request->getContent())) {
|
||||||
|
json_decode($request->getContent(), true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
throw new BadRequestHttpException(
|
||||||
|
sprintf(
|
||||||
|
'The JSON data passed in the request appears to be malformed. err_code: %d err_message: "%s"',
|
||||||
|
json_last_error(),
|
||||||
|
json_last_error_msg()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
@ -1,59 +0,0 @@
|
|||||||
<?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\Http\Middleware\Server;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
|
|
||||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
|
|
||||||
class AuthenticateAsSubuser
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService
|
|
||||||
*/
|
|
||||||
private $keyProviderService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SubuserAccessAuthenticate constructor.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService
|
|
||||||
*/
|
|
||||||
public function __construct(DaemonKeyProviderService $keyProviderService)
|
|
||||||
{
|
|
||||||
$this->keyProviderService = $keyProviderService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a subuser has permissions to access a server, if so set their access token.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return mixed
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next)
|
|
||||||
{
|
|
||||||
$server = $request->attributes->get('server');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$token = $this->keyProviderService->handle($server, $request->user());
|
|
||||||
} catch (RecordNotFoundException $exception) {
|
|
||||||
throw new AccessDeniedHttpException('This account does not have permission to access this server.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$request->attributes->set('server_token', $token);
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Http\Middleware\Server;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
|
|
||||||
class DatabaseBelongsToServer
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
|
|
||||||
*/
|
|
||||||
private $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DatabaseAccess constructor.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
|
|
||||||
*/
|
|
||||||
public function __construct(DatabaseRepositoryInterface $repository)
|
|
||||||
{
|
|
||||||
$this->repository = $repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a database being requested belongs to the currently loaded server.
|
|
||||||
* If it does not, throw a 404 error, otherwise continue on with the request
|
|
||||||
* and set an attribute with the database.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return mixed
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next)
|
|
||||||
{
|
|
||||||
$server = $request->attributes->get('server');
|
|
||||||
$database = $request->input('database') ?? $request->route()->parameter('database');
|
|
||||||
|
|
||||||
if (! is_digit($database)) {
|
|
||||||
throw new NotFoundHttpException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$database = $this->repository->find($database);
|
|
||||||
if (is_null($database) || $database->server_id !== $server->id) {
|
|
||||||
throw new NotFoundHttpException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$request->attributes->set('database', $database);
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Http\Middleware\Server;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
|
||||||
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
|
|
||||||
class ScheduleBelongsToServer
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Extensions\HashidsInterface
|
|
||||||
*/
|
|
||||||
private $hashids;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
|
|
||||||
*/
|
|
||||||
private $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TaskAccess constructor.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
|
|
||||||
*/
|
|
||||||
public function __construct(HashidsInterface $hashids, ScheduleRepositoryInterface $repository)
|
|
||||||
{
|
|
||||||
$this->hashids = $hashids;
|
|
||||||
$this->repository = $repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a task is assigned to the active server.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return mixed
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next)
|
|
||||||
{
|
|
||||||
$server = $request->attributes->get('server');
|
|
||||||
|
|
||||||
$scheduleId = $this->hashids->decodeFirst($request->route()->parameter('schedule'), 0);
|
|
||||||
$schedule = $this->repository->getScheduleWithTasks($scheduleId);
|
|
||||||
|
|
||||||
if ($schedule->server_id !== $server->id) {
|
|
||||||
throw new NotFoundHttpException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$request->attributes->set('schedule', $schedule);
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Http\Middleware\Server;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Pterodactyl\Exceptions\DisplayException;
|
|
||||||
use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
|
||||||
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
|
|
||||||
class SubuserBelongsToServer
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Extensions\HashidsInterface
|
|
||||||
*/
|
|
||||||
private $hashids;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
|
|
||||||
*/
|
|
||||||
private $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SubuserAccess constructor.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository
|
|
||||||
*/
|
|
||||||
public function __construct(HashidsInterface $hashids, SubuserRepositoryInterface $repository)
|
|
||||||
{
|
|
||||||
$this->hashids = $hashids;
|
|
||||||
$this->repository = $repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a user has permission to access and modify subuser.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return mixed
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next)
|
|
||||||
{
|
|
||||||
$server = $request->attributes->get('server');
|
|
||||||
|
|
||||||
$hash = $request->route()->parameter('subuser', 0);
|
|
||||||
$subuser = $this->repository->find($this->hashids->decodeFirst($hash, 0));
|
|
||||||
if (is_null($subuser) || $subuser->server_id !== $server->id) {
|
|
||||||
throw new NotFoundHttpException;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->method() === 'PATCH') {
|
|
||||||
if ($subuser->user_id === $request->user()->id) {
|
|
||||||
throw new DisplayException(trans('exceptions.subusers.editing_self'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$request->attributes->set('subuser', $subuser);
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,7 +21,7 @@ class StoreNodeRequest extends ApplicationApiRequest
|
|||||||
/**
|
/**
|
||||||
* Validation rules to apply to this request.
|
* Validation rules to apply to this request.
|
||||||
*
|
*
|
||||||
* @param null|array $rules
|
* @param array|null $rules
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function rules(array $rules = null): array
|
public function rules(array $rules = null): array
|
||||||
|
@ -21,7 +21,7 @@ class StoreScheduleRequest extends ViewScheduleRequest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => 'required|string|min:1',
|
'name' => 'required|string|min:1',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'filled|boolean',
|
||||||
'minute' => 'required|string',
|
'minute' => 'required|string',
|
||||||
'hour' => 'required|string',
|
'hour' => 'required|string',
|
||||||
'day_of_month' => 'required|string',
|
'day_of_month' => 'required|string',
|
||||||
|
@ -25,7 +25,7 @@ class StoreTaskRequest extends ViewScheduleRequest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'action' => 'required|in:command,power,backup',
|
'action' => 'required|in:command,power,backup',
|
||||||
'payload' => 'required_unless:action,backup|string',
|
'payload' => 'required_unless:action,backup|string|nullable',
|
||||||
'time_offset' => 'required|numeric|min:0|max:900',
|
'time_offset' => 'required|numeric|min:0|max:900',
|
||||||
'sequence_id' => 'sometimes|required|numeric|min:1',
|
'sequence_id' => 'sometimes|required|numeric|min:1',
|
||||||
];
|
];
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers;
|
namespace Pterodactyl\Http\Requests\Api\Client\Servers;
|
||||||
|
|
||||||
use Pterodactyl\Models\Permission;
|
use Pterodactyl\Models\Permission;
|
||||||
|
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
|
|
||||||
class SendCommandRequest extends GetServerRequest
|
class SendCommandRequest extends ClientApiRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Determine if the API user has permission to perform this action.
|
* Determine if the API user has permission to perform this action.
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Pterodactyl\Http\Requests\Auth;
|
namespace Pterodactyl\Http\Requests\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class LoginCheckpointRequest extends FormRequest
|
class LoginCheckpointRequest extends FormRequest
|
||||||
@ -25,7 +26,20 @@ class LoginCheckpointRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'confirmation_token' => 'required|string',
|
'confirmation_token' => 'required|string',
|
||||||
'authentication_code' => 'required|numeric',
|
'authentication_code' => [
|
||||||
|
'nullable',
|
||||||
|
'numeric',
|
||||||
|
Rule::requiredIf(function () {
|
||||||
|
return empty($this->input('recovery_token'));
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
'recovery_token' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
Rule::requiredIf(function () {
|
||||||
|
return empty($this->input('authentication_code'));
|
||||||
|
}),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
namespace Pterodactyl\Models;
|
namespace Pterodactyl\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $server_id
|
||||||
|
* @property int $database_host_id
|
||||||
|
* @property string $database
|
||||||
|
* @property string $username
|
||||||
|
* @property string $remote
|
||||||
|
* @property string $password
|
||||||
|
* @property int $max_connections
|
||||||
|
* @property \Carbon\Carbon $created_at
|
||||||
|
* @property \Carbon\Carbon $updated_at
|
||||||
|
*
|
||||||
|
* @property \Pterodactyl\Models\Server $server
|
||||||
|
* @property \Pterodactyl\Models\DatabaseHost $host
|
||||||
|
*/
|
||||||
class Database extends Model
|
class Database extends Model
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
namespace Pterodactyl\Models;
|
namespace Pterodactyl\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $short
|
||||||
|
* @property string $long
|
||||||
|
* @property \Carbon\Carbon $created_at
|
||||||
|
* @property \Carbon\Carbon $updated_at
|
||||||
|
*
|
||||||
|
* @property \Pterodactyl\Models\Node[] $nodes
|
||||||
|
* @property \Pterodactyl\Models\Server[] $servers
|
||||||
|
*/
|
||||||
class Location extends Model
|
class Location extends Model
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
39
app/Models/RecoveryToken.php
Normal file
39
app/Models/RecoveryToken.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $user_id
|
||||||
|
* @property string $token
|
||||||
|
* @property \Carbon\CarbonImmutable $created_at
|
||||||
|
*
|
||||||
|
* @property \Pterodactyl\Models\User $user
|
||||||
|
*/
|
||||||
|
class RecoveryToken extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* There are no updates to this model, only inserts and deletes.
|
||||||
|
*/
|
||||||
|
const UPDATED_AT = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $immutableDates = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
public static $validationRules = [
|
||||||
|
'token' => 'required|string',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,7 @@ namespace Pterodactyl\Models;
|
|||||||
*
|
*
|
||||||
* @property \Pterodactyl\Models\Server $server
|
* @property \Pterodactyl\Models\Server $server
|
||||||
*/
|
*/
|
||||||
class ServerTransfer extends Validable
|
class ServerTransfer extends Model
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource name for this model when it is transformed into an
|
* The resource name for this model when it is transformed into an
|
||||||
|
@ -90,7 +90,7 @@ class Task extends Model
|
|||||||
'schedule_id' => 'required|numeric|exists:schedules,id',
|
'schedule_id' => 'required|numeric|exists:schedules,id',
|
||||||
'sequence_id' => 'required|numeric|min:1',
|
'sequence_id' => 'required|numeric|min:1',
|
||||||
'action' => 'required|string',
|
'action' => 'required|string',
|
||||||
'payload' => 'required|string',
|
'payload' => 'required_unless:action,backup|string',
|
||||||
'time_offset' => 'required|numeric|between:0,900',
|
'time_offset' => 'required|numeric|between:0,900',
|
||||||
'is_queued' => 'boolean',
|
'is_queued' => 'boolean',
|
||||||
];
|
];
|
||||||
|
@ -39,6 +39,7 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
|
|||||||
* @property \Pterodactyl\Models\ApiKey[]|\Illuminate\Database\Eloquent\Collection $apiKeys
|
* @property \Pterodactyl\Models\ApiKey[]|\Illuminate\Database\Eloquent\Collection $apiKeys
|
||||||
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
|
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
|
||||||
* @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys
|
* @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys
|
||||||
|
* @property \Pterodactyl\Models\RecoveryToken[]|\Illuminate\Database\Eloquent\Collection $recoveryTokens
|
||||||
*/
|
*/
|
||||||
class User extends Model implements
|
class User extends Model implements
|
||||||
AuthenticatableContract,
|
AuthenticatableContract,
|
||||||
@ -164,7 +165,7 @@ class User extends Model implements
|
|||||||
'name_last' => 'required|string|between:1,255',
|
'name_last' => 'required|string|between:1,255',
|
||||||
'password' => 'sometimes|nullable|string',
|
'password' => 'sometimes|nullable|string',
|
||||||
'root_admin' => 'boolean',
|
'root_admin' => 'boolean',
|
||||||
'language' => 'required|string',
|
'language' => 'string',
|
||||||
'use_totp' => 'boolean',
|
'use_totp' => 'boolean',
|
||||||
'totp_secret' => 'nullable|string',
|
'totp_secret' => 'nullable|string',
|
||||||
];
|
];
|
||||||
@ -251,4 +252,12 @@ class User extends Model implements
|
|||||||
return $this->hasMany(ApiKey::class)
|
return $this->hasMany(ApiKey::class)
|
||||||
->where('key_type', ApiKey::TYPE_ACCOUNT);
|
->where('key_type', ApiKey::TYPE_ACCOUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function recoveryTokens()
|
||||||
|
{
|
||||||
|
return $this->hasMany(RecoveryToken::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,16 +33,22 @@ class RouteServiceProvider extends ServiceProvider
|
|||||||
->namespace($this->namespace . '\Auth')
|
->namespace($this->namespace . '\Auth')
|
||||||
->group(base_path('routes/auth.php'));
|
->group(base_path('routes/auth.php'));
|
||||||
|
|
||||||
Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser.auth', 'node.maintenance'])
|
Route::middleware(['web', 'csrf', 'auth', 'server', 'node.maintenance'])
|
||||||
->prefix('/api/server/{server}')
|
->prefix('/api/server/{server}')
|
||||||
->namespace($this->namespace . '\Server')
|
->namespace($this->namespace . '\Server')
|
||||||
->group(base_path('routes/server.php'));
|
->group(base_path('routes/server.php'));
|
||||||
|
|
||||||
Route::middleware(['api'])->prefix('/api/application')
|
Route::middleware([
|
||||||
|
sprintf('throttle:%s,%s', config('http.rate_limit.application'), config('http.rate_limit.application_period')),
|
||||||
|
'api',
|
||||||
|
])->prefix('/api/application')
|
||||||
->namespace($this->namespace . '\Api\Application')
|
->namespace($this->namespace . '\Api\Application')
|
||||||
->group(base_path('routes/api-application.php'));
|
->group(base_path('routes/api-application.php'));
|
||||||
|
|
||||||
Route::middleware(['client-api'])->prefix('/api/client')
|
Route::middleware([
|
||||||
|
sprintf('throttle:%s,%s', config('http.rate_limit.client'), config('http.rate_limit.client_period')),
|
||||||
|
'client-api',
|
||||||
|
])->prefix('/api/client')
|
||||||
->namespace($this->namespace . '\Api\Client')
|
->namespace($this->namespace . '\Api\Client')
|
||||||
->group(base_path('routes/api-client.php'));
|
->group(base_path('routes/api-client.php'));
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ trait Searchable
|
|||||||
/**
|
/**
|
||||||
* The search term to use when filtering results.
|
* The search term to use when filtering results.
|
||||||
*
|
*
|
||||||
* @var null|string
|
* @var string|null
|
||||||
*/
|
*/
|
||||||
protected $searchTerm;
|
protected $searchTerm;
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
|
|||||||
*/
|
*/
|
||||||
public function createUser(string $username, string $remote, string $password, $max_connections): bool
|
public function createUser(string $username, string $remote, string $password, $max_connections): bool
|
||||||
{
|
{
|
||||||
if (!$max_connections) {
|
if (! $max_connections) {
|
||||||
return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password));
|
return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password));
|
||||||
} else {
|
} else {
|
||||||
return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\' WITH MAX_USER_CONNECTIONS %s', $username, $remote, $password, $max_connections));
|
return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\' WITH MAX_USER_CONNECTIONS %s', $username, $remote, $password, $max_connections));
|
||||||
|
16
app/Repositories/Eloquent/RecoveryTokenRepository.php
Normal file
16
app/Repositories/Eloquent/RecoveryTokenRepository.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Repositories\Eloquent;
|
||||||
|
|
||||||
|
use Pterodactyl\Models\RecoveryToken;
|
||||||
|
|
||||||
|
class RecoveryTokenRepository extends EloquentRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function model()
|
||||||
|
{
|
||||||
|
return RecoveryToken::class;
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@ class SessionRepository extends EloquentRepository implements SessionRepositoryI
|
|||||||
*
|
*
|
||||||
* @param int $user
|
* @param int $user
|
||||||
* @param string $session
|
* @param string $session
|
||||||
* @return null|int
|
* @return int|null
|
||||||
*/
|
*/
|
||||||
public function deleteUserSession(int $user, string $session)
|
public function deleteUserSession(int $user, string $session)
|
||||||
{
|
{
|
||||||
|
@ -41,7 +41,7 @@ class TaskRepository extends EloquentRepository implements TaskRepositoryInterfa
|
|||||||
*
|
*
|
||||||
* @param int $schedule
|
* @param int $schedule
|
||||||
* @param int $index
|
* @param int $index
|
||||||
* @return null|\Pterodactyl\Models\Task
|
* @return \Pterodactyl\Models\Task|null
|
||||||
*/
|
*/
|
||||||
public function getNextTask(int $schedule, int $index)
|
public function getNextTask(int $schedule, int $index)
|
||||||
{
|
{
|
||||||
|
@ -34,7 +34,7 @@ class DaemonConfigurationRepository extends DaemonRepository
|
|||||||
* @return \Psr\Http\Message\ResponseInterface
|
* @return \Psr\Http\Message\ResponseInterface
|
||||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||||
*/
|
*/
|
||||||
public function update(?Node $node)
|
public function update(Node $node)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return $this->getHttpClient()->post(
|
return $this->getHttpClient()->post(
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* Pterodactyl - Panel
|
|
||||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Pterodactyl\Services\DaemonKeys;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
|
||||||
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
|
|
||||||
|
|
||||||
class DaemonKeyCreationService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Carbon\Carbon
|
|
||||||
*/
|
|
||||||
protected $carbon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Illuminate\Contracts\Config\Repository
|
|
||||||
*/
|
|
||||||
protected $config;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
|
|
||||||
*/
|
|
||||||
protected $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DaemonKeyCreationService constructor.
|
|
||||||
*
|
|
||||||
* @param \Carbon\Carbon $carbon
|
|
||||||
* @param \Illuminate\Contracts\Config\Repository $config
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
Carbon $carbon,
|
|
||||||
ConfigRepository $config,
|
|
||||||
DaemonKeyRepositoryInterface $repository
|
|
||||||
) {
|
|
||||||
$this->carbon = $carbon;
|
|
||||||
$this->config = $config;
|
|
||||||
$this->repository = $repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new daemon key to be used when connecting to a daemon.
|
|
||||||
*
|
|
||||||
* @param int $server
|
|
||||||
* @param int $user
|
|
||||||
* @return string
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
*/
|
|
||||||
public function handle(int $server, int $user)
|
|
||||||
{
|
|
||||||
$secret = DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40);
|
|
||||||
|
|
||||||
$this->repository->withoutFreshModel()->create([
|
|
||||||
'user_id' => $user,
|
|
||||||
'server_id' => $server,
|
|
||||||
'secret' => $secret,
|
|
||||||
'expires_at' => $this->carbon->now()->addMinutes($this->config->get('pterodactyl.api.key_expire_time'))->toDateTimeString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $secret;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* Pterodactyl - Panel
|
|
||||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Pterodactyl\Services\DaemonKeys;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Pterodactyl\Models\User;
|
|
||||||
use Pterodactyl\Models\Server;
|
|
||||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
|
||||||
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
|
|
||||||
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
|
|
||||||
|
|
||||||
class DaemonKeyProviderService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService
|
|
||||||
*/
|
|
||||||
private $keyCreationService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService
|
|
||||||
*/
|
|
||||||
private $keyUpdateService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
|
|
||||||
*/
|
|
||||||
private $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
|
|
||||||
*/
|
|
||||||
private $subuserRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GetDaemonKeyService constructor.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService $keyCreationService
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository
|
|
||||||
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService $keyUpdateService
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $subuserRepository
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
DaemonKeyCreationService $keyCreationService,
|
|
||||||
DaemonKeyRepositoryInterface $repository,
|
|
||||||
DaemonKeyUpdateService $keyUpdateService,
|
|
||||||
SubuserRepositoryInterface $subuserRepository
|
|
||||||
) {
|
|
||||||
$this->keyCreationService = $keyCreationService;
|
|
||||||
$this->keyUpdateService = $keyUpdateService;
|
|
||||||
$this->repository = $repository;
|
|
||||||
$this->subuserRepository = $subuserRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the access key for a user on a specific server.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Models\Server $server
|
|
||||||
* @param \Pterodactyl\Models\User $user
|
|
||||||
* @param bool $updateIfExpired
|
|
||||||
* @return string
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
*/
|
|
||||||
public function handle(Server $server, User $user, $updateIfExpired = true): string
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$key = $this->repository->findFirstWhere([
|
|
||||||
['user_id', '=', $user->id],
|
|
||||||
['server_id', '=', $server->id],
|
|
||||||
]);
|
|
||||||
} catch (RecordNotFoundException $exception) {
|
|
||||||
// If key doesn't exist but we are an admin or the server owner,
|
|
||||||
// create it.
|
|
||||||
if ($user->root_admin || $user->id === $server->owner_id) {
|
|
||||||
return $this->keyCreationService->handle($server->id, $user->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is a subuser for this server. Ideally they should always have
|
|
||||||
// a record associated with them in the database, but we should still handle
|
|
||||||
// that potentiality here.
|
|
||||||
//
|
|
||||||
// If no subuser is found, a RecordNotFoundException will be thrown, thus handling
|
|
||||||
// the parent error as well.
|
|
||||||
$subuser = $this->subuserRepository->findFirstWhere([
|
|
||||||
['user_id', '=', $user->id],
|
|
||||||
['server_id', '=', $server->id],
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->keyCreationService->handle($subuser->server_id, $subuser->user_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $updateIfExpired || Carbon::now()->diffInSeconds($key->expires_at, false) > 0) {
|
|
||||||
return $key->secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->keyUpdateService->handle($key->id);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* Pterodactyl - Panel
|
|
||||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Pterodactyl\Services\DaemonKeys;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
|
||||||
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
|
|
||||||
|
|
||||||
class DaemonKeyUpdateService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Carbon\Carbon
|
|
||||||
*/
|
|
||||||
protected $carbon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Illuminate\Contracts\Config\Repository
|
|
||||||
*/
|
|
||||||
protected $config;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
|
|
||||||
*/
|
|
||||||
protected $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DaemonKeyUpdateService constructor.
|
|
||||||
*
|
|
||||||
* @param \Carbon\Carbon $carbon
|
|
||||||
* @param \Illuminate\Contracts\Config\Repository $config
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
Carbon $carbon,
|
|
||||||
ConfigRepository $config,
|
|
||||||
DaemonKeyRepositoryInterface $repository
|
|
||||||
) {
|
|
||||||
$this->carbon = $carbon;
|
|
||||||
$this->config = $config;
|
|
||||||
$this->repository = $repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a daemon key to expire the previous one.
|
|
||||||
*
|
|
||||||
* @param int $key
|
|
||||||
* @return string
|
|
||||||
*
|
|
||||||
* @throws \RuntimeException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
*/
|
|
||||||
public function handle($key)
|
|
||||||
{
|
|
||||||
Assert::integerish($key, 'First argument passed to handle must be an integer, received %s.');
|
|
||||||
|
|
||||||
$secret = DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40);
|
|
||||||
$this->repository->withoutFreshModel()->update($key, [
|
|
||||||
'secret' => $secret,
|
|
||||||
'expires_at' => $this->carbon->now()->addMinutes($this->config->get('pterodactyl.api.key_expire_time'))->toDateTimeString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $secret;
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,19 +3,22 @@
|
|||||||
namespace Pterodactyl\Services\Databases;
|
namespace Pterodactyl\Services\Databases;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Pterodactyl\Models\Server;
|
||||||
use Pterodactyl\Models\Database;
|
use Pterodactyl\Models\Database;
|
||||||
use Pterodactyl\Helpers\Utilities;
|
use Pterodactyl\Helpers\Utilities;
|
||||||
use Illuminate\Database\DatabaseManager;
|
use Illuminate\Database\ConnectionInterface;
|
||||||
use Illuminate\Contracts\Encryption\Encrypter;
|
use Illuminate\Contracts\Encryption\Encrypter;
|
||||||
use Pterodactyl\Extensions\DynamicDatabaseConnection;
|
use Pterodactyl\Extensions\DynamicDatabaseConnection;
|
||||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
||||||
|
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
|
||||||
|
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
|
||||||
|
|
||||||
class DatabaseManagementService
|
class DatabaseManagementService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var \Illuminate\Database\DatabaseManager
|
* @var \Illuminate\Database\ConnectionInterface
|
||||||
*/
|
*/
|
||||||
private $database;
|
private $connection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
|
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
|
||||||
@ -33,84 +36,113 @@ class DatabaseManagementService
|
|||||||
private $repository;
|
private $repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Determines if the service should validate the user's ability to create an additional
|
||||||
|
* database for this server. In almost all cases this should be true, but to keep things
|
||||||
|
* flexible you can also set it to false and create more databases than the server is
|
||||||
|
* allocated.
|
||||||
|
*
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
protected $useRandomHost = false;
|
protected $validateDatabaseLimit = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CreationService constructor.
|
* CreationService constructor.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Database\DatabaseManager $database
|
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||||
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
|
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
|
||||||
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
|
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
|
||||||
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
DatabaseManager $database,
|
ConnectionInterface $connection,
|
||||||
DynamicDatabaseConnection $dynamic,
|
DynamicDatabaseConnection $dynamic,
|
||||||
DatabaseRepositoryInterface $repository,
|
DatabaseRepositoryInterface $repository,
|
||||||
Encrypter $encrypter
|
Encrypter $encrypter
|
||||||
) {
|
) {
|
||||||
$this->database = $database;
|
$this->connection = $connection;
|
||||||
$this->dynamic = $dynamic;
|
$this->dynamic = $dynamic;
|
||||||
$this->encrypter = $encrypter;
|
$this->encrypter = $encrypter;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set wether or not this class should validate that the server has enough slots
|
||||||
|
* left before creating the new database.
|
||||||
|
*
|
||||||
|
* @param bool $validate
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setValidateDatabaseLimit(bool $validate): self
|
||||||
|
{
|
||||||
|
$this->validateDatabaseLimit = $validate;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new database that is linked to a specific host.
|
* Create a new database that is linked to a specific host.
|
||||||
*
|
*
|
||||||
* @param int $server
|
* @param \Pterodactyl\Models\Server $server
|
||||||
* @param array $data
|
* @param array $data
|
||||||
* @return \Pterodactyl\Models\Database
|
* @return \Pterodactyl\Models\Database
|
||||||
*
|
*
|
||||||
* @throws \Exception
|
* @throws \Throwable
|
||||||
|
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
||||||
*/
|
*/
|
||||||
public function create($server, array $data)
|
public function create(Server $server, array $data)
|
||||||
{
|
{
|
||||||
$data['server_id'] = $server;
|
if (! config('pterodactyl.client_features.databases.enabled')) {
|
||||||
$data['database'] = sprintf('s%d_%s', $server, $data['database']);
|
throw new DatabaseClientFeatureNotEnabledException;
|
||||||
$data['username'] = sprintf('u%d_%s', $server, str_random(10));
|
}
|
||||||
$data['password'] = $this->encrypter->encrypt(
|
|
||||||
Utilities::randomStringWithSpecialCharacters(24)
|
if ($this->validateDatabaseLimit) {
|
||||||
);
|
// If the server has a limit assigned and we've already reached that limit, throw back
|
||||||
|
// an exception and kill the process.
|
||||||
|
if (! is_null($server->database_limit) && $server->databases()->count() >= $server->database_limit) {
|
||||||
|
throw new TooManyDatabasesException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array_merge($data, [
|
||||||
|
'server_id' => $server->id,
|
||||||
|
'database' => sprintf('s%d_%s', $server->id, $data['database']),
|
||||||
|
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
|
||||||
|
'password' => $this->encrypter->encrypt(
|
||||||
|
Utilities::randomStringWithSpecialCharacters(24)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$database = null;
|
||||||
|
|
||||||
$this->database->beginTransaction();
|
|
||||||
try {
|
try {
|
||||||
$database = $this->repository->createIfNotExists($data);
|
return $this->connection->transaction(function () use ($data, &$database) {
|
||||||
$this->dynamic->set('dynamic', $data['database_host_id']);
|
$database = $this->repository->createIfNotExists($data);
|
||||||
|
$this->dynamic->set('dynamic', $data['database_host_id']);
|
||||||
|
|
||||||
$this->repository->createDatabase($database->database);
|
$this->repository->createDatabase($database->database);
|
||||||
$this->repository->createUser(
|
$this->repository->createUser(
|
||||||
$database->username,
|
$database->username, $database->remote, $this->encrypter->decrypt($database->password), $database->max_connections
|
||||||
$database->remote,
|
);
|
||||||
$this->encrypter->decrypt($database->password),
|
$this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);
|
||||||
$database->max_connections
|
$this->repository->flush();
|
||||||
);
|
|
||||||
$this->repository->assignUserToDatabase(
|
|
||||||
$database->database,
|
|
||||||
$database->username,
|
|
||||||
$database->remote
|
|
||||||
);
|
|
||||||
$this->repository->flush();
|
|
||||||
|
|
||||||
$this->database->commit();
|
return $database;
|
||||||
} catch (Exception $ex) {
|
});
|
||||||
|
} catch (Exception $exception) {
|
||||||
try {
|
try {
|
||||||
if (isset($database) && $database instanceof Database) {
|
if ($database instanceof Database) {
|
||||||
$this->repository->dropDatabase($database->database);
|
$this->repository->dropDatabase($database->database);
|
||||||
$this->repository->dropUser($database->username, $database->remote);
|
$this->repository->dropUser($database->username, $database->remote);
|
||||||
$this->repository->flush();
|
$this->repository->flush();
|
||||||
}
|
}
|
||||||
} catch (Exception $exTwo) {
|
} catch (Exception $exception) {
|
||||||
// ignore an exception
|
// Do nothing here. We've already encountered an issue before this point so no
|
||||||
|
// reason to prioritize this error over the initial one.
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->database->rollBack();
|
throw $exception;
|
||||||
throw $ex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $database;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,9 +6,7 @@ use Pterodactyl\Models\Server;
|
|||||||
use Pterodactyl\Models\Database;
|
use Pterodactyl\Models\Database;
|
||||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
||||||
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
|
||||||
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
|
|
||||||
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
|
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
|
||||||
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
|
|
||||||
|
|
||||||
class DeployServerDatabaseService
|
class DeployServerDatabaseService
|
||||||
{
|
{
|
||||||
@ -49,20 +47,12 @@ class DeployServerDatabaseService
|
|||||||
* @param array $data
|
* @param array $data
|
||||||
* @return \Pterodactyl\Models\Database
|
* @return \Pterodactyl\Models\Database
|
||||||
*
|
*
|
||||||
|
* @throws \Throwable
|
||||||
|
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
|
||||||
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
||||||
* @throws \Exception
|
|
||||||
*/
|
*/
|
||||||
public function handle(Server $server, array $data): Database
|
public function handle(Server $server, array $data): Database
|
||||||
{
|
{
|
||||||
if (! config('pterodactyl.client_features.databases.enabled')) {
|
|
||||||
throw new DatabaseClientFeatureNotEnabledException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$databases = $this->repository->findCountWhere([['server_id', '=', $server->id]]);
|
|
||||||
if (! is_null($server->database_limit) && $databases >= $server->database_limit) {
|
|
||||||
throw new TooManyDatabasesException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$allowRandom = config('pterodactyl.client_features.databases.allow_random');
|
$allowRandom = config('pterodactyl.client_features.databases.allow_random');
|
||||||
$hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([
|
$hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([
|
||||||
['node_id', '=', $server->node_id],
|
['node_id', '=', $server->node_id],
|
||||||
@ -81,7 +71,7 @@ class DeployServerDatabaseService
|
|||||||
|
|
||||||
$host = $hosts->random();
|
$host = $hosts->random();
|
||||||
|
|
||||||
return $this->managementService->create($server->id, [
|
return $this->managementService->create($server, [
|
||||||
'database_host_id' => $host->id,
|
'database_host_id' => $host->id,
|
||||||
'database' => array_get($data, 'database'),
|
'database' => array_get($data, 'database'),
|
||||||
'remote' => array_get($data, 'remote'),
|
'remote' => array_get($data, 'remote'),
|
||||||
|
@ -76,7 +76,16 @@ class EggImporterService
|
|||||||
public function handle(UploadedFile $file, int $nest): Egg
|
public function handle(UploadedFile $file, int $nest): Egg
|
||||||
{
|
{
|
||||||
if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) {
|
if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) {
|
||||||
throw new InvalidFileUploadException(trans('exceptions.nest.importer.file_error'));
|
throw new InvalidFileUploadException(
|
||||||
|
sprintf(
|
||||||
|
'The selected file ["%s"] was not in a valid format to import. (is_file: %s is_valid: %s err_code: %s err: %s)',
|
||||||
|
$file->getFilename(),
|
||||||
|
$file->isFile() ? 'true' : 'false',
|
||||||
|
$file->isValid() ? 'true' : 'false',
|
||||||
|
$file->getError(),
|
||||||
|
$file->getErrorMessage()
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$parsed = json_decode($file->openFile()->fread($file->getSize()));
|
$parsed = json_decode($file->openFile()->fread($file->getSize()));
|
||||||
|
@ -57,7 +57,16 @@ class EggUpdateImporterService
|
|||||||
public function handle(int $egg, UploadedFile $file)
|
public function handle(int $egg, UploadedFile $file)
|
||||||
{
|
{
|
||||||
if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) {
|
if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) {
|
||||||
throw new InvalidFileUploadException(trans('exceptions.nest.importer.file_error'));
|
throw new InvalidFileUploadException(
|
||||||
|
sprintf(
|
||||||
|
'The selected file ["%s"] was not in a valid format to import. (is_file: %s is_valid: %s err_code: %s err: %s)',
|
||||||
|
$file->getFilename(),
|
||||||
|
$file->isFile() ? 'true' : 'false',
|
||||||
|
$file->isValid() ? 'true' : 'false',
|
||||||
|
$file->getError(),
|
||||||
|
$file->getErrorMessage()
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$parsed = json_decode($file->openFile()->fread($file->getSize()));
|
$parsed = json_decode($file->openFile()->fread($file->getSize()));
|
||||||
|
@ -24,7 +24,7 @@ class AssetHashService
|
|||||||
private $application;
|
private $application;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var null|array
|
* @var array|null
|
||||||
*/
|
*/
|
||||||
protected static $manifest;
|
protected static $manifest;
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ namespace Pterodactyl\Services\Helpers;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use Cake\Chronos\Chronos;
|
use Carbon\CarbonImmutable;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||||
use Pterodactyl\Exceptions\Service\Helper\CdnVersionFetchingException;
|
use Pterodactyl\Exceptions\Service\Helper\CdnVersionFetchingException;
|
||||||
@ -120,7 +120,7 @@ class SoftwareVersionService
|
|||||||
*/
|
*/
|
||||||
protected function cacheVersionData()
|
protected function cacheVersionData()
|
||||||
{
|
{
|
||||||
return $this->cache->remember(self::VERSION_CACHE_KEY, Chronos::now()->addMinutes(config()->get('pterodactyl.cdn.cache_time', 60)), function () {
|
return $this->cache->remember(self::VERSION_CACHE_KEY, CarbonImmutable::now()->addMinutes(config()->get('pterodactyl.cdn.cache_time', 60)), function () {
|
||||||
try {
|
try {
|
||||||
$response = $this->client->request('GET', config()->get('pterodactyl.cdn.url'));
|
$response = $this->client->request('GET', config()->get('pterodactyl.cdn.url'));
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ namespace Pterodactyl\Services\Nodes;
|
|||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Pterodactyl\Models\Node;
|
use Pterodactyl\Models\Node;
|
||||||
use Illuminate\Encryption\Encrypter;
|
use Illuminate\Contracts\Encryption\Encrypter;
|
||||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
||||||
|
|
||||||
class NodeCreationService
|
class NodeCreationService
|
||||||
@ -16,14 +16,14 @@ class NodeCreationService
|
|||||||
protected $repository;
|
protected $repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Illuminate\Encryption\Encrypter
|
* @var \Illuminate\Contracts\Encryption\Encrypter
|
||||||
*/
|
*/
|
||||||
private $encrypter;
|
private $encrypter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CreationService constructor.
|
* CreationService constructor.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Encryption\Encrypter $encrypter
|
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
||||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
||||||
*/
|
*/
|
||||||
public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository)
|
public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository)
|
||||||
|
@ -71,8 +71,8 @@ class BuildModificationService
|
|||||||
* @return \Pterodactyl\Models\Server
|
* @return \Pterodactyl\Models\Server
|
||||||
*
|
*
|
||||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
*/
|
*/
|
||||||
public function handle(Server $server, array $data)
|
public function handle(Server $server, array $data)
|
||||||
{
|
{
|
||||||
@ -91,7 +91,7 @@ class BuildModificationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var \Pterodactyl\Models\Server $server */
|
/* @var \Pterodactyl\Models\Server $server */
|
||||||
$server = $this->repository->withFreshModel()->update($server->id, [
|
$server = $this->repository->withFreshModel()->update($server->id, [
|
||||||
'oom_disabled' => array_get($data, 'oom_disabled'),
|
'oom_disabled' => array_get($data, 'oom_disabled'),
|
||||||
'memory' => array_get($data, 'memory'),
|
'memory' => array_get($data, 'memory'),
|
||||||
|
@ -4,8 +4,8 @@ namespace Pterodactyl\Services\Servers;
|
|||||||
|
|
||||||
use Pterodactyl\Models\Server;
|
use Pterodactyl\Models\Server;
|
||||||
use Illuminate\Database\ConnectionInterface;
|
use Illuminate\Database\ConnectionInterface;
|
||||||
|
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
|
||||||
|
|
||||||
class ReinstallServerService
|
class ReinstallServerService
|
||||||
{
|
{
|
||||||
@ -17,27 +17,27 @@ class ReinstallServerService
|
|||||||
/**
|
/**
|
||||||
* @var \Illuminate\Database\ConnectionInterface
|
* @var \Illuminate\Database\ConnectionInterface
|
||||||
*/
|
*/
|
||||||
private $database;
|
private $connection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
||||||
*/
|
*/
|
||||||
private $repository;
|
private $repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ReinstallService constructor.
|
* ReinstallService constructor.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Database\ConnectionInterface $database
|
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||||
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
||||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
|
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ConnectionInterface $database,
|
ConnectionInterface $connection,
|
||||||
DaemonServerRepository $daemonServerRepository,
|
DaemonServerRepository $daemonServerRepository,
|
||||||
ServerRepositoryInterface $repository
|
ServerRepository $repository
|
||||||
) {
|
) {
|
||||||
$this->daemonServerRepository = $daemonServerRepository;
|
$this->daemonServerRepository = $daemonServerRepository;
|
||||||
$this->database = $database;
|
$this->connection = $connection;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,14 +51,14 @@ class ReinstallServerService
|
|||||||
*/
|
*/
|
||||||
public function reinstall(Server $server)
|
public function reinstall(Server $server)
|
||||||
{
|
{
|
||||||
$this->database->transaction(function () use ($server) {
|
return $this->connection->transaction(function () use ($server) {
|
||||||
$this->repository->withoutFreshModel()->update($server->id, [
|
$updated = $this->repository->update($server->id, [
|
||||||
'installed' => Server::STATUS_INSTALLING,
|
'installed' => Server::STATUS_INSTALLING,
|
||||||
]);
|
], true, true);
|
||||||
|
|
||||||
$this->daemonServerRepository->setServer($server)->reinstall();
|
$this->daemonServerRepository->setServer($server)->reinstall();
|
||||||
});
|
|
||||||
|
|
||||||
return $server->refresh();
|
return $updated;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -310,8 +310,6 @@ class ServerCreationService
|
|||||||
return $allocation->node_id;
|
return $allocation->node_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @noinspection PhpDocMissingThrowsInspection */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a unique UUID and UUID-Short combo for a server.
|
* Create a unique UUID and UUID-Short combo for a server.
|
||||||
*
|
*
|
||||||
@ -319,7 +317,6 @@ class ServerCreationService
|
|||||||
*/
|
*/
|
||||||
private function generateUniqueUuidCombo(): string
|
private function generateUniqueUuidCombo(): string
|
||||||
{
|
{
|
||||||
/** @noinspection PhpUnhandledExceptionInspection */
|
|
||||||
$uuid = Uuid::uuid4()->toString();
|
$uuid = Uuid::uuid4()->toString();
|
||||||
|
|
||||||
if (! $this->repository->isUniqueUuidCombo($uuid, substr($uuid, 0, 8))) {
|
if (! $this->repository->isUniqueUuidCombo($uuid, substr($uuid, 0, 8))) {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Pterodactyl\Services\Servers;
|
namespace Pterodactyl\Services\Servers;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Pterodactyl\Models\Server;
|
use Pterodactyl\Models\Server;
|
||||||
use Illuminate\Database\ConnectionInterface;
|
use Illuminate\Database\ConnectionInterface;
|
||||||
@ -109,7 +110,15 @@ class ServerDeletionService
|
|||||||
|
|
||||||
$this->connection->transaction(function () use ($server) {
|
$this->connection->transaction(function () use ($server) {
|
||||||
$this->databaseRepository->setColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) {
|
$this->databaseRepository->setColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) {
|
||||||
$this->databaseManagementService->delete($item->id);
|
try {
|
||||||
|
$this->databaseManagementService->delete($item->id);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
if ($this->force) {
|
||||||
|
$this->writer->warning($exception);
|
||||||
|
} else {
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->repository->delete($server->id);
|
$this->repository->delete($server->id);
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
namespace Pterodactyl\Services\Users;
|
namespace Pterodactyl\Services\Users;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Pterodactyl\Models\User;
|
use Pterodactyl\Models\User;
|
||||||
use PragmaRX\Google2FA\Google2FA;
|
use PragmaRX\Google2FA\Google2FA;
|
||||||
|
use Illuminate\Database\ConnectionInterface;
|
||||||
use Illuminate\Contracts\Encryption\Encrypter;
|
use Illuminate\Contracts\Encryption\Encrypter;
|
||||||
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
|
||||||
|
use Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository;
|
||||||
use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
|
use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
|
||||||
|
|
||||||
class ToggleTwoFactorService
|
class ToggleTwoFactorService
|
||||||
@ -26,21 +29,37 @@ class ToggleTwoFactorService
|
|||||||
*/
|
*/
|
||||||
private $repository;
|
private $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository
|
||||||
|
*/
|
||||||
|
private $recoveryTokenRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Illuminate\Database\ConnectionInterface
|
||||||
|
*/
|
||||||
|
private $connection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ToggleTwoFactorService constructor.
|
* ToggleTwoFactorService constructor.
|
||||||
*
|
*
|
||||||
|
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||||
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
||||||
* @param \PragmaRX\Google2FA\Google2FA $google2FA
|
* @param \PragmaRX\Google2FA\Google2FA $google2FA
|
||||||
|
* @param \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository $recoveryTokenRepository
|
||||||
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
|
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
ConnectionInterface $connection,
|
||||||
Encrypter $encrypter,
|
Encrypter $encrypter,
|
||||||
Google2FA $google2FA,
|
Google2FA $google2FA,
|
||||||
|
RecoveryTokenRepository $recoveryTokenRepository,
|
||||||
UserRepositoryInterface $repository
|
UserRepositoryInterface $repository
|
||||||
) {
|
) {
|
||||||
$this->encrypter = $encrypter;
|
$this->encrypter = $encrypter;
|
||||||
$this->google2FA = $google2FA;
|
$this->google2FA = $google2FA;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->recoveryTokenRepository = $recoveryTokenRepository;
|
||||||
|
$this->connection = $connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,32 +68,60 @@ class ToggleTwoFactorService
|
|||||||
* @param \Pterodactyl\Models\User $user
|
* @param \Pterodactyl\Models\User $user
|
||||||
* @param string $token
|
* @param string $token
|
||||||
* @param bool|null $toggleState
|
* @param bool|null $toggleState
|
||||||
* @return bool
|
* @return string[]
|
||||||
*
|
*
|
||||||
|
* @throws \Throwable
|
||||||
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
|
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
|
||||||
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
|
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
|
||||||
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
|
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
|
* @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
|
||||||
*/
|
*/
|
||||||
public function handle(User $user, string $token, bool $toggleState = null): bool
|
public function handle(User $user, string $token, bool $toggleState = null): array
|
||||||
{
|
{
|
||||||
$secret = $this->encrypter->decrypt($user->totp_secret);
|
$secret = $this->encrypter->decrypt($user->totp_secret);
|
||||||
|
|
||||||
$isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window'));
|
$isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window'));
|
||||||
|
|
||||||
if (! $isValidToken) {
|
if (! $isValidToken) {
|
||||||
throw new TwoFactorAuthenticationTokenInvalid(
|
throw new TwoFactorAuthenticationTokenInvalid('The token provided is not valid.');
|
||||||
'The token provided is not valid.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->repository->withoutFreshModel()->update($user->id, [
|
return $this->connection->transaction(function () use ($user, $toggleState) {
|
||||||
'totp_authenticated_at' => Carbon::now(),
|
// Now that we're enabling 2FA on the account, generate 10 recovery tokens for the account
|
||||||
'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState),
|
// and store them hashed in the database. We'll return them to the caller so that the user
|
||||||
]);
|
// can see and save them.
|
||||||
|
//
|
||||||
|
// If a user is unable to login with a 2FA token they can provide one of these backup codes
|
||||||
|
// which will then be marked as deleted from the database and will also bypass 2FA protections
|
||||||
|
// on their account.
|
||||||
|
$tokens = [];
|
||||||
|
if ((! $toggleState && ! $user->use_totp) || $toggleState) {
|
||||||
|
$inserts = [];
|
||||||
|
for ($i = 0; $i < 10; $i++) {
|
||||||
|
$token = Str::random(10);
|
||||||
|
|
||||||
return true;
|
$inserts[] = [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'token' => password_hash($token, PASSWORD_DEFAULT),
|
||||||
|
];
|
||||||
|
|
||||||
|
$tokens[] = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before inserting any new records make sure all of the old ones are deleted to avoid
|
||||||
|
// any issues or storing an unnecessary number of tokens in the database.
|
||||||
|
$this->recoveryTokenRepository->deleteWhere(['user_id' => $user->id]);
|
||||||
|
|
||||||
|
// Bulk insert the hashed tokens.
|
||||||
|
$this->recoveryTokenRepository->insert($inserts);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->repository->withoutFreshModel()->update($user->id, [
|
||||||
|
'totp_authenticated_at' => Carbon::now(),
|
||||||
|
'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ class TwoFactorSetupService
|
|||||||
'totp_secret' => $this->encrypter->encrypt($secret),
|
'totp_secret' => $this->encrypter->encrypt($secret),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$company = preg_replace('/\s/', '', $this->config->get('app.name'));
|
$company = urlencode(preg_replace('/\s/', '', $this->config->get('app.name')));
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'otpauth://totp/%1$s:%2$s?secret=%3$s&issuer=%1$s',
|
'otpauth://totp/%1$s:%2$s?secret=%3$s&issuer=%1$s',
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Pterodactyl\Transformers\Api\Application;
|
namespace Pterodactyl\Transformers\Api\Application;
|
||||||
|
|
||||||
|
use Pterodactyl\Models\Node;
|
||||||
|
use Pterodactyl\Models\Server;
|
||||||
use Pterodactyl\Models\Allocation;
|
use Pterodactyl\Models\Allocation;
|
||||||
use Pterodactyl\Services\Acl\Api\AdminAcl;
|
use Pterodactyl\Services\Acl\Api\AdminAcl;
|
||||||
|
|
||||||
@ -54,10 +56,8 @@ class AllocationTransformer extends BaseTransformer
|
|||||||
return $this->null();
|
return $this->null();
|
||||||
}
|
}
|
||||||
|
|
||||||
$allocation->loadMissing('node');
|
|
||||||
|
|
||||||
return $this->item(
|
return $this->item(
|
||||||
$allocation->getRelation('node'), $this->makeTransformer(NodeTransformer::class), 'node'
|
$allocation->node, $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,14 +70,12 @@ class AllocationTransformer extends BaseTransformer
|
|||||||
*/
|
*/
|
||||||
public function includeServer(Allocation $allocation)
|
public function includeServer(Allocation $allocation)
|
||||||
{
|
{
|
||||||
if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) {
|
if (! $this->authorize(AdminAcl::RESOURCE_SERVERS) || ! $allocation->server) {
|
||||||
return $this->null();
|
return $this->null();
|
||||||
}
|
}
|
||||||
|
|
||||||
$allocation->loadMissing('server');
|
|
||||||
|
|
||||||
return $this->item(
|
return $this->item(
|
||||||
$allocation->getRelation('server'), $this->makeTransformer(ServerTransformer::class), 'server'
|
$allocation->server, $this->makeTransformer(ServerTransformer::class), Server::RESOURCE_NAME
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,22 @@ $kernel->bootstrap();
|
|||||||
|
|
||||||
$output = new ConsoleOutput;
|
$output = new ConsoleOutput;
|
||||||
|
|
||||||
|
if (config('database.default') !== 'testing') {
|
||||||
|
$output->writeln(PHP_EOL . '<error>Cannot run test process against non-testing database.</error>');
|
||||||
|
$output->writeln(PHP_EOL . '<error>Environment is currently pointed at: "' . config('database.default') . '".</error>');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Perform database migrations and reseeding before continuing with
|
* Perform database migrations and reseeding before continuing with
|
||||||
* running the tests.
|
* running the tests.
|
||||||
*/
|
*/
|
||||||
$output->writeln(PHP_EOL . '<comment>Refreshing database for Integration tests...</comment>');
|
if (! env('SKIP_MIGRATIONS')) {
|
||||||
$kernel->call('migrate:fresh', ['--database' => 'testing']);
|
$output->writeln(PHP_EOL . '<info>Refreshing database for Integration tests...</info>');
|
||||||
|
$kernel->call('migrate:fresh', ['--database' => 'testing']);
|
||||||
|
|
||||||
$output->writeln('<comment>Seeding database for Integration tests...</comment>' . PHP_EOL);
|
$output->writeln('<info>Seeding database for Integration tests...</info>' . PHP_EOL);
|
||||||
$kernel->call('db:seed', ['--database' => 'testing']);
|
$kernel->call('db:seed', ['--database' => 'testing']);
|
||||||
|
} else {
|
||||||
|
$output->writeln(PHP_EOL . '<comment>Skipping database migrations...</comment>' . PHP_EOL);
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.2",
|
"php": "^7.2",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"ext-pdo_mysql": "*",
|
"ext-pdo_mysql": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
@ -23,9 +23,10 @@
|
|||||||
"guzzlehttp/guzzle": "^6.5",
|
"guzzlehttp/guzzle": "^6.5",
|
||||||
"hashids/hashids": "^4.0",
|
"hashids/hashids": "^4.0",
|
||||||
"laracasts/utilities": "^3.1",
|
"laracasts/utilities": "^3.1",
|
||||||
"laravel/framework": "^6.18",
|
"laravel/framework": "^7.17",
|
||||||
"laravel/helpers": "^1.2",
|
"laravel/helpers": "^1.2",
|
||||||
"laravel/tinker": "^1.0",
|
"laravel/tinker": "^2.4",
|
||||||
|
"laravel/ui": "^2.0",
|
||||||
"lcobucci/jwt": "^3.3",
|
"lcobucci/jwt": "^3.3",
|
||||||
"league/flysystem-aws-s3-v3": "^1.0",
|
"league/flysystem-aws-s3-v3": "^1.0",
|
||||||
"league/flysystem-memory": "^1.0",
|
"league/flysystem-memory": "^1.0",
|
||||||
@ -33,22 +34,23 @@
|
|||||||
"pragmarx/google2fa": "^5.0",
|
"pragmarx/google2fa": "^5.0",
|
||||||
"predis/predis": "^1.1",
|
"predis/predis": "^1.1",
|
||||||
"prologue/alerts": "^0.4",
|
"prologue/alerts": "^0.4",
|
||||||
|
"psy/psysh": "^0.10.4",
|
||||||
"s1lentium/iptools": "^1.1",
|
"s1lentium/iptools": "^1.1",
|
||||||
"spatie/laravel-fractal": "^5.7",
|
"spatie/laravel-fractal": "^5.7",
|
||||||
"staudenmeir/belongs-to-through": "^2.9",
|
"staudenmeir/belongs-to-through": "^2.10",
|
||||||
"symfony/yaml": "^4.4",
|
"symfony/yaml": "^4.4",
|
||||||
"webmozart/assert": "^1.7"
|
"webmozart/assert": "^1.9"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^3.2",
|
"barryvdh/laravel-debugbar": "^3.3",
|
||||||
"barryvdh/laravel-ide-helper": "^2.6",
|
"barryvdh/laravel-ide-helper": "^2.7",
|
||||||
"codedungeon/phpunit-result-printer": "0.25.1",
|
"codedungeon/phpunit-result-printer": "^0.28.0",
|
||||||
"friendsofphp/php-cs-fixer": "^2.16.1",
|
"friendsofphp/php-cs-fixer": "2.16.1",
|
||||||
"fzaninotto/faker": "^1.9.1",
|
"fzaninotto/faker": "^1.9",
|
||||||
"laravel/dusk": "^5.11",
|
"laravel/dusk": "^6.3",
|
||||||
"mockery/mockery": "^1.0",
|
"mockery/mockery": "^1.4",
|
||||||
"php-mock/php-mock-phpunit": "^2.6",
|
"php-mock/php-mock-phpunit": "^2.6",
|
||||||
"phpunit/phpunit": "^7"
|
"phpunit/phpunit": "^8.5"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
|
2909
composer.lock
generated
2909
composer.lock
generated
File diff suppressed because it is too large
Load Diff
21
config/http.php
Normal file
21
config/http.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| API Rate Limits
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Defines the rate limit for the number of requests per minute that can be
|
||||||
|
| executed against both the client and internal (application) APIs over the
|
||||||
|
| defined period (by default, 1 minute).
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'rate_limit' => [
|
||||||
|
'client_period' => 1,
|
||||||
|
'client' => env('APP_API_CLIENT_RATELIMIT', 240),
|
||||||
|
|
||||||
|
'application_period' => 1,
|
||||||
|
'application' => env('APP_API_APPLICATION_RATELIMIT', 240),
|
||||||
|
],
|
||||||
|
];
|
@ -3,7 +3,6 @@
|
|||||||
use Monolog\Handler\StreamHandler;
|
use Monolog\Handler\StreamHandler;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Default Log Channel
|
| Default Log Channel
|
||||||
@ -77,5 +76,4 @@ return [
|
|||||||
'level' => 'debug',
|
'level' => 'debug',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -162,6 +162,11 @@ return [
|
|||||||
'enabled' => env('PTERODACTYL_CLIENT_DATABASES_ENABLED', true),
|
'enabled' => env('PTERODACTYL_CLIENT_DATABASES_ENABLED', true),
|
||||||
'allow_random' => env('PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM', true),
|
'allow_random' => env('PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM', true),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'schedules' => [
|
||||||
|
// The total number of tasks that can exist for any given schedule at once.
|
||||||
|
'per_schedule_task_limit' => 10,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -218,5 +223,7 @@ return [
|
|||||||
|
|
|
|
||||||
| 'P_SERVER_CREATED_AT' => 'created_at'
|
| 'P_SERVER_CREATED_AT' => 'created_at'
|
||||||
*/
|
*/
|
||||||
'environment_variables' => [],
|
'environment_variables' => [
|
||||||
|
'P_SERVER_ALLOCATION_LIMIT' => 'allocation_limit',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -20,9 +20,7 @@ use Pterodactyl\Models\ApiKey;
|
|||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
'uuid' => Uuid::uuid4()->toString(),
|
||||||
'node_id' => $faker->randomNumber(),
|
|
||||||
'uuid' => $faker->unique()->uuid,
|
|
||||||
'uuidShort' => str_random(8),
|
'uuidShort' => str_random(8),
|
||||||
'name' => $faker->firstName,
|
'name' => $faker->firstName,
|
||||||
'description' => implode(' ', $faker->sentences()),
|
'description' => implode(' ', $faker->sentences()),
|
||||||
@ -34,9 +32,6 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
|
|||||||
'io' => 500,
|
'io' => 500,
|
||||||
'cpu' => 0,
|
'cpu' => 0,
|
||||||
'oom_disabled' => 0,
|
'oom_disabled' => 0,
|
||||||
'allocation_id' => $faker->randomNumber(),
|
|
||||||
'nest_id' => $faker->randomNumber(),
|
|
||||||
'egg_id' => $faker->randomNumber(),
|
|
||||||
'pack_id' => null,
|
'pack_id' => null,
|
||||||
'installed' => 1,
|
'installed' => 1,
|
||||||
'database_limit' => null,
|
'database_limit' => null,
|
||||||
@ -50,7 +45,6 @@ $factory->define(Pterodactyl\Models\User::class, function (Faker $faker) {
|
|||||||
static $password;
|
static $password;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'external_id' => $faker->unique()->isbn10,
|
'external_id' => $faker->unique()->isbn10,
|
||||||
'uuid' => $faker->uuid,
|
'uuid' => $faker->uuid,
|
||||||
'username' => $faker->userName,
|
'username' => $faker->userName,
|
||||||
@ -74,15 +68,13 @@ $factory->state(Pterodactyl\Models\User::class, 'admin', function () {
|
|||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Location::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Location::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
'short' => Str::random(8),
|
||||||
'short' => $faker->unique()->domainWord,
|
|
||||||
'long' => $faker->catchPhrase,
|
'long' => $faker->catchPhrase,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'uuid' => Uuid::uuid4()->toString(),
|
'uuid' => Uuid::uuid4()->toString(),
|
||||||
'public' => true,
|
'public' => true,
|
||||||
'name' => $faker->firstName,
|
'name' => $faker->firstName,
|
||||||
@ -95,7 +87,7 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
|
|||||||
'disk_overallocate' => 0,
|
'disk_overallocate' => 0,
|
||||||
'upload_size' => 100,
|
'upload_size' => 100,
|
||||||
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
|
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
|
||||||
'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH),
|
'daemon_token' => encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH)),
|
||||||
'daemonListen' => 8080,
|
'daemonListen' => 8080,
|
||||||
'daemonSFTP' => 2022,
|
'daemonSFTP' => 2022,
|
||||||
'daemonBase' => '/var/lib/pterodactyl/volumes',
|
'daemonBase' => '/var/lib/pterodactyl/volumes',
|
||||||
@ -104,7 +96,6 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
|
|||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Nest::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Nest::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'uuid' => $faker->unique()->uuid,
|
'uuid' => $faker->unique()->uuid,
|
||||||
'author' => 'testauthor@example.com',
|
'author' => 'testauthor@example.com',
|
||||||
'name' => $faker->word,
|
'name' => $faker->word,
|
||||||
@ -114,9 +105,7 @@ $factory->define(Pterodactyl\Models\Nest::class, function (Faker $faker) {
|
|||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Egg::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Egg::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'uuid' => $faker->unique()->uuid,
|
'uuid' => $faker->unique()->uuid,
|
||||||
'nest_id' => $faker->unique()->randomNumber(),
|
|
||||||
'name' => $faker->name,
|
'name' => $faker->name,
|
||||||
'description' => implode(' ', $faker->sentences(3)),
|
'description' => implode(' ', $faker->sentences(3)),
|
||||||
'startup' => 'java -jar test.jar',
|
'startup' => 'java -jar test.jar',
|
||||||
@ -125,7 +114,6 @@ $factory->define(Pterodactyl\Models\Egg::class, function (Faker $faker) {
|
|||||||
|
|
||||||
$factory->define(Pterodactyl\Models\EggVariable::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\EggVariable::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'name' => $faker->firstName,
|
'name' => $faker->firstName,
|
||||||
'description' => $faker->sentence(),
|
'description' => $faker->sentence(),
|
||||||
'env_variable' => strtoupper(str_replace(' ', '_', $faker->words(2, true))),
|
'env_variable' => strtoupper(str_replace(' ', '_', $faker->words(2, true))),
|
||||||
@ -146,8 +134,6 @@ $factory->state(Pterodactyl\Models\EggVariable::class, 'editable', function () {
|
|||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Pack::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Pack::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'egg_id' => $faker->randomNumber(),
|
|
||||||
'uuid' => $faker->uuid,
|
'uuid' => $faker->uuid,
|
||||||
'name' => $faker->word,
|
'name' => $faker->word,
|
||||||
'description' => null,
|
'description' => null,
|
||||||
@ -159,17 +145,11 @@ $factory->define(Pterodactyl\Models\Pack::class, function (Faker $faker) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Subuser::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Subuser::class, function (Faker $faker) {
|
||||||
return [
|
return [];
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'user_id' => $faker->randomNumber(),
|
|
||||||
'server_id' => $faker->randomNumber(),
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Allocation::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Allocation::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'node_id' => $faker->randomNumber(),
|
|
||||||
'ip' => $faker->ipv4,
|
'ip' => $faker->ipv4,
|
||||||
'port' => $faker->randomNumber(5),
|
'port' => $faker->randomNumber(5),
|
||||||
];
|
];
|
||||||
@ -177,13 +157,11 @@ $factory->define(Pterodactyl\Models\Allocation::class, function (Faker $faker) {
|
|||||||
|
|
||||||
$factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'name' => $faker->colorName,
|
'name' => $faker->colorName,
|
||||||
'host' => $faker->unique()->ipv4,
|
'host' => $faker->unique()->ipv4,
|
||||||
'port' => 3306,
|
'port' => 3306,
|
||||||
'username' => $faker->colorName,
|
'username' => $faker->colorName,
|
||||||
'password' => Crypt::encrypt($faker->word),
|
'password' => Crypt::encrypt($faker->word),
|
||||||
'node_id' => $faker->randomNumber(),
|
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -191,9 +169,6 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) {
|
|||||||
static $password;
|
static $password;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'server_id' => $faker->randomNumber(),
|
|
||||||
'database_host_id' => $faker->randomNumber(),
|
|
||||||
'database' => str_random(10),
|
'database' => str_random(10),
|
||||||
'username' => str_random(10),
|
'username' => str_random(10),
|
||||||
'remote' => '%',
|
'remote' => '%',
|
||||||
@ -205,16 +180,12 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) {
|
|||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Schedule::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Schedule::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'server_id' => $faker->randomNumber(),
|
|
||||||
'name' => $faker->firstName(),
|
'name' => $faker->firstName(),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$factory->define(Pterodactyl\Models\Task::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Task::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'schedule_id' => $faker->randomNumber(),
|
|
||||||
'sequence_id' => $faker->randomNumber(1),
|
'sequence_id' => $faker->randomNumber(1),
|
||||||
'action' => 'command',
|
'action' => 'command',
|
||||||
'payload' => 'test command',
|
'payload' => 'test command',
|
||||||
@ -225,9 +196,6 @@ $factory->define(Pterodactyl\Models\Task::class, function (Faker $faker) {
|
|||||||
|
|
||||||
$factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'server_id' => $faker->randomNumber(),
|
|
||||||
'user_id' => $faker->randomNumber(),
|
|
||||||
'secret' => 'i_' . str_random(40),
|
'secret' => 'i_' . str_random(40),
|
||||||
'expires_at' => \Carbon\Carbon::now()->addMinutes(10)->toDateTimeString(),
|
'expires_at' => \Carbon\Carbon::now()->addMinutes(10)->toDateTimeString(),
|
||||||
];
|
];
|
||||||
@ -237,8 +205,6 @@ $factory->define(Pterodactyl\Models\ApiKey::class, function (Faker $faker) {
|
|||||||
static $token;
|
static $token;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
|
||||||
'user_id' => $faker->randomNumber(),
|
|
||||||
'key_type' => ApiKey::TYPE_APPLICATION,
|
'key_type' => ApiKey::TYPE_APPLICATION,
|
||||||
'identifier' => str_random(Pterodactyl\Models\ApiKey::IDENTIFIER_LENGTH),
|
'identifier' => str_random(Pterodactyl\Models\ApiKey::IDENTIFIER_LENGTH),
|
||||||
'token' => $token ?: $token = encrypt(str_random(Pterodactyl\Models\ApiKey::KEY_LENGTH)),
|
'token' => $token ?: $token = encrypt(str_random(Pterodactyl\Models\ApiKey::KEY_LENGTH)),
|
||||||
|
@ -23,6 +23,5 @@ class DeleteTaskWhenParentServerIsDeleted extends Migration
|
|||||||
*/
|
*/
|
||||||
public function down()
|
public function down()
|
||||||
{
|
{
|
||||||
//
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
class AllowNullableDescriptions extends Migration
|
class AllowNullableDescriptions extends Migration
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
class AddMaxConnectionsColumn extends Migration
|
class AddMaxConnectionsColumn extends Migration
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
class AddBackupLimitToServers extends Migration
|
class AddBackupLimitToServers extends Migration
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateUserRecoveryTokensTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('recovery_tokens', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedInteger('user_id');
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
|
||||||
|
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('recovery_tokens');
|
||||||
|
}
|
||||||
|
}
|
@ -112,14 +112,16 @@ class EggSeeder extends Seeder
|
|||||||
$files = $this->filesystem->allFiles(database_path('seeds/eggs/' . kebab_case($nest->name)));
|
$files = $this->filesystem->allFiles(database_path('seeds/eggs/' . kebab_case($nest->name)));
|
||||||
|
|
||||||
$this->command->alert('Updating Eggs for Nest: ' . $nest->name);
|
$this->command->alert('Updating Eggs for Nest: ' . $nest->name);
|
||||||
collect($files)->each(function ($file) use ($nest) {
|
Collection::make($files)->each(function ($file) use ($nest) {
|
||||||
/* @var \Symfony\Component\Finder\SplFileInfo $file */
|
/* @var \Symfony\Component\Finder\SplFileInfo $file */
|
||||||
$decoded = json_decode($file->getContents());
|
$decoded = json_decode($file->getContents());
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
return $this->command->error('JSON decode exception for ' . $file->getFilename() . ': ' . json_last_error_msg());
|
$this->command->error('JSON decode exception for ' . $file->getFilename() . ': ' . json_last_error_msg());
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json', $file->getSize());
|
$file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$egg = $this->repository->setColumns('id')->findFirstWhere([
|
$egg = $this->repository->setColumns('id')->findFirstWhere([
|
||||||
@ -130,11 +132,11 @@ class EggSeeder extends Seeder
|
|||||||
|
|
||||||
$this->updateImporterService->handle($egg->id, $file);
|
$this->updateImporterService->handle($egg->id, $file);
|
||||||
|
|
||||||
return $this->command->info('Updated ' . $decoded->name);
|
$this->command->info('Updated ' . $decoded->name);
|
||||||
} catch (RecordNotFoundException $exception) {
|
} catch (RecordNotFoundException $exception) {
|
||||||
$this->importerService->handle($file, $nest->id);
|
$this->importerService->handle($file, $nest->id);
|
||||||
|
|
||||||
return $this->command->comment('Created ' . $decoded->name);
|
$this->command->comment('Created ' . $decoded->name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
"name": "Server Version",
|
"name": "Server Version",
|
||||||
"description": "Version of Mumble Server to download and use.",
|
"description": "Version of Mumble Server to download and use.",
|
||||||
"env_variable": "MUMBLE_VERSION",
|
"env_variable": "MUMBLE_VERSION",
|
||||||
"default_value": "1.2.19",
|
"default_value": "1.3.1",
|
||||||
"user_viewable": 1,
|
"user_viewable": 1,
|
||||||
"user_editable": 1,
|
"user_editable": 1,
|
||||||
"rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/"
|
"rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/"
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
#terminal > .cmd {
|
#terminal > .cmd {
|
||||||
padding: 1px 0;
|
padding: 1px 0;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#terminal_input {
|
#terminal_input {
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
export default (code: string): Promise<void> => {
|
export default async (code: string): Promise<string[]> => {
|
||||||
return new Promise((resolve, reject) => {
|
const { data } = await http.post('/api/client/account/two-factor', { code });
|
||||||
http.post('/api/client/account/two-factor', { code })
|
|
||||||
.then(() => resolve())
|
return data.attributes.tokens;
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
import { LoginResponse } from '@/api/auth/login';
|
import { LoginResponse } from '@/api/auth/login';
|
||||||
|
|
||||||
export default (token: string, code: string): Promise<LoginResponse> => {
|
export default (token: string, code: string, recoveryToken?: string): Promise<LoginResponse> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/auth/login/checkpoint', {
|
http.post('/auth/login/checkpoint', {
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
confirmation_token: token,
|
confirmation_token: token,
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
authentication_code: code,
|
authentication_code: code,
|
||||||
|
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
|
||||||
|
/* eslint-enable @typescript-eslint/camelcase */
|
||||||
})
|
})
|
||||||
.then(response => resolve({
|
.then(response => resolve({
|
||||||
complete: response.data.data.complete,
|
complete: response.data.data.complete,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import loginCheckpoint from '@/api/auth/loginCheckpoint';
|
import loginCheckpoint from '@/api/auth/loginCheckpoint';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
@ -14,6 +14,7 @@ import Field from '@/components/elements/Field';
|
|||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
code: string;
|
code: string;
|
||||||
|
recoveryCode: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }>
|
type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }>
|
||||||
@ -24,7 +25,8 @@ type Props = OwnProps & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LoginCheckpointContainer = () => {
|
const LoginCheckpointContainer = () => {
|
||||||
const { isSubmitting } = useFormikContext<Values>();
|
const { isSubmitting, setFieldValue } = useFormikContext<Values>();
|
||||||
|
const [ isMissingDevice, setIsMissingDevice ] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginFormContainer
|
<LoginFormContainer
|
||||||
@ -34,10 +36,14 @@ const LoginCheckpointContainer = () => {
|
|||||||
<div className={'mt-6'}>
|
<div className={'mt-6'}>
|
||||||
<Field
|
<Field
|
||||||
light={true}
|
light={true}
|
||||||
name={'code'}
|
name={isMissingDevice ? 'recoveryCode' : 'code'}
|
||||||
title={'Authentication Code'}
|
title={isMissingDevice ? 'Recovery Code' : 'Authentication Code'}
|
||||||
description={'Enter the two-factor token generated by your device.'}
|
description={
|
||||||
type={'number'}
|
isMissingDevice
|
||||||
|
? 'Enter one of the recovery codes generated when you setup 2-Factor authentication on this account in order to continue.'
|
||||||
|
: 'Enter the two-factor token generated by your device.'
|
||||||
|
}
|
||||||
|
type={isMissingDevice ? 'text' : 'number'}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -54,6 +60,18 @@ const LoginCheckpointContainer = () => {
|
|||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={'mt-6 text-center'}>
|
||||||
|
<span
|
||||||
|
onClick={() => {
|
||||||
|
setFieldValue('code', '');
|
||||||
|
setFieldValue('recoveryCode', '');
|
||||||
|
setIsMissingDevice(s => !s);
|
||||||
|
}}
|
||||||
|
className={'cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
|
||||||
|
>
|
||||||
|
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className={'mt-6 text-center'}>
|
<div className={'mt-6 text-center'}>
|
||||||
<Link
|
<Link
|
||||||
to={'/auth/login'}
|
to={'/auth/login'}
|
||||||
@ -67,10 +85,9 @@ const LoginCheckpointContainer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EnhancedForm = withFormik<Props, Values>({
|
const EnhancedForm = withFormik<Props, Values>({
|
||||||
handleSubmit: ({ code }, { setSubmitting, props: { addError, clearFlashes, location } }) => {
|
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { addError, clearFlashes, location } }) => {
|
||||||
clearFlashes();
|
clearFlashes();
|
||||||
console.log(location.state.token, code);
|
loginCheckpoint(location.state?.token || '', code, recoveryCode)
|
||||||
loginCheckpoint(location.state?.token || '', code)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.complete) {
|
if (response.complete) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -89,11 +106,7 @@ const EnhancedForm = withFormik<Props, Values>({
|
|||||||
|
|
||||||
mapPropsToValues: () => ({
|
mapPropsToValues: () => ({
|
||||||
code: '',
|
code: '',
|
||||||
}),
|
recoveryCode: '',
|
||||||
|
|
||||||
validationSchema: object().shape({
|
|
||||||
code: string().required('An authentication code must be provided.')
|
|
||||||
.length(6, 'Authentication code must be 6 digits in length.'),
|
|
||||||
}),
|
}),
|
||||||
})(LoginCheckpointContainer);
|
})(LoginCheckpointContainer);
|
||||||
|
|
||||||
|
@ -52,6 +52,8 @@ export default ({ server, className }: { server: Server; className: string | und
|
|||||||
alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);
|
alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);
|
||||||
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
|
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
|
||||||
}
|
}
|
||||||
|
const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited";
|
||||||
|
const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/server/${server.id}`} className={`grey-row-box cursor-pointer ${className}`}>
|
<Link to={`/server/${server.id}`} className={`grey-row-box cursor-pointer ${className}`}>
|
||||||
@ -127,7 +129,7 @@ export default ({ server, className }: { server: Server; className: string | und
|
|||||||
{bytesToHuman(stats.memoryUsageInBytes)}
|
{bytesToHuman(stats.memoryUsageInBytes)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {bytesToHuman(server.limits.memory * 1000 * 1000)}</p>
|
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {memorylimit}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex-1 ml-4'}>
|
<div className={'flex-1 ml-4'}>
|
||||||
<div className={'flex justify-center'}>
|
<div className={'flex justify-center'}>
|
||||||
@ -147,9 +149,7 @@ export default ({ server, className }: { server: Server; className: string | und
|
|||||||
{bytesToHuman(stats.diskUsageInBytes)}
|
{bytesToHuman(stats.diskUsageInBytes)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className={'text-xs text-neutral-600 text-center mt-1'}>
|
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {disklimit}</p>
|
||||||
of {bytesToHuman(server.limits.disk * 1000 * 1000)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,22 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||||
import { Form, Formik, FormikHelpers } from 'formik';
|
import { Form, Formik, FormikHelpers } from 'formik';
|
||||||
import { object, string } from 'yup';
|
import { object, string } from 'yup';
|
||||||
import Field from '@/components/elements/Field';
|
|
||||||
import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
|
import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
|
||||||
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
|
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
import { Actions, useStoreActions } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
import Field from '@/components/elements/Field';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ ...props }: RequiredModalProps) => {
|
export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
||||||
const [ token, setToken ] = useState('');
|
const [ token, setToken ] = useState('');
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
|
const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]);
|
||||||
|
|
||||||
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
|
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
|
||||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
@ -27,22 +28,30 @@ export default ({ ...props }: RequiredModalProps) => {
|
|||||||
.then(setToken)
|
.then(setToken)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
clearFlashes('account:two-factor');
|
clearFlashes('account:two-factor');
|
||||||
enableAccountTwoFactor(code)
|
enableAccountTwoFactor(code)
|
||||||
.then(() => {
|
.then(tokens => {
|
||||||
updateUserData({ useTotp: true });
|
setRecoveryTokens(tokens);
|
||||||
props.onDismissed();
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
|
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
|
||||||
setSubmitting(false);
|
})
|
||||||
});
|
.then(() => setSubmitting(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
if (recoveryTokens.length > 0) {
|
||||||
|
updateUserData({ useTotp: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDismissed();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -58,47 +67,73 @@ export default ({ ...props }: RequiredModalProps) => {
|
|||||||
{({ isSubmitting, isValid }) => (
|
{({ isSubmitting, isValid }) => (
|
||||||
<Modal
|
<Modal
|
||||||
{...props}
|
{...props}
|
||||||
|
onDismissed={dismiss}
|
||||||
dismissable={!isSubmitting}
|
dismissable={!isSubmitting}
|
||||||
showSpinnerOverlay={loading || isSubmitting}
|
showSpinnerOverlay={loading || isSubmitting}
|
||||||
|
closeOnEscape={!recoveryTokens}
|
||||||
|
closeOnBackground={!recoveryTokens}
|
||||||
>
|
>
|
||||||
<Form className={'mb-0'}>
|
{recoveryTokens.length > 0 ?
|
||||||
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
|
<>
|
||||||
<div className={'flex flex-wrap'}>
|
<h2 className={'mb-4'}>Two-factor authentication enabled</h2>
|
||||||
<div className={'w-full md:flex-1'}>
|
<p className={'text-neutral-300'}>
|
||||||
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
|
Two-factor authentication has been enabled on your account. Should you loose access to
|
||||||
{!token || !token.length ?
|
this device you'll need to use on of the codes displayed below in order to access your
|
||||||
<img
|
account.
|
||||||
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
|
</p>
|
||||||
className={'w-64 h-64 rounded'}
|
<p className={'text-neutral-300 mt-4'}>
|
||||||
|
<strong>These codes will not be displayed again.</strong> Please take note of them now
|
||||||
|
by storing them in a secure repository such as a password manager.
|
||||||
|
</p>
|
||||||
|
<pre className={'mt-4 rounded font-mono bg-neutral-900 p-4'}>
|
||||||
|
{recoveryTokens.map(token => <code key={token} className={'block mb-1'}>{token}</code>)}
|
||||||
|
</pre>
|
||||||
|
<div className={'text-right'}>
|
||||||
|
<button className={'mt-6 btn btn-lg btn-primary'} onClick={dismiss}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<Form className={'mb-0'}>
|
||||||
|
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
|
||||||
|
<div className={'flex flex-wrap'}>
|
||||||
|
<div className={'w-full md:flex-1'}>
|
||||||
|
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
|
||||||
|
{!token || !token.length ?
|
||||||
|
<img
|
||||||
|
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
|
||||||
|
className={'w-64 h-64 rounded'}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<img
|
||||||
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
className={'w-full h-full shadow-none rounded-0'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
|
||||||
|
<div className={'flex-1'}>
|
||||||
|
<Field
|
||||||
|
id={'code'}
|
||||||
|
name={'code'}
|
||||||
|
type={'text'}
|
||||||
|
title={'Code From Authenticator'}
|
||||||
|
description={'Enter the code from your authenticator device after scanning the QR image.'}
|
||||||
|
autoFocus={!loading}
|
||||||
/>
|
/>
|
||||||
:
|
</div>
|
||||||
<img
|
<div className={'mt-6 md:mt-0 text-right'}>
|
||||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
|
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
|
||||||
onLoad={() => setLoading(false)}
|
Setup
|
||||||
className={'w-full h-full shadow-none rounded-0'}
|
</button>
|
||||||
/>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
|
</Form>
|
||||||
<div className={'flex-1'}>
|
}
|
||||||
<Field
|
|
||||||
id={'code'}
|
|
||||||
name={'code'}
|
|
||||||
type={'text'}
|
|
||||||
title={'Code From Authenticator'}
|
|
||||||
description={'Enter the code from your authenticator device after scanning the QR image.'}
|
|
||||||
autoFocus={!loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={'mt-6 md:mt-0 text-right'}>
|
|
||||||
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
|
|
||||||
Setup
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
@ -81,6 +81,9 @@ export default () => {
|
|||||||
};
|
};
|
||||||
}, [ instance, connected ]);
|
}, [ instance, connected ]);
|
||||||
|
|
||||||
|
const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited";
|
||||||
|
const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock className={'flex'}>
|
<PageContentBlock className={'flex'}>
|
||||||
<div className={'w-1/4'}>
|
<div className={'w-1/4'}>
|
||||||
@ -112,8 +115,8 @@ export default () => {
|
|||||||
className={'mr-1'}
|
className={'mr-1'}
|
||||||
/>
|
/>
|
||||||
{bytesToHuman(memory)}
|
{bytesToHuman(memory)}
|
||||||
<span className={'text-neutral-500'}> / {bytesToHuman(server.limits.memory * 1000 * 1000)}</span>
|
<span className={'text-neutral-500'}> / {memorylimit}</span>
|
||||||
</p>
|
</p>
|
||||||
<p className={'text-xs mt-2'}>
|
<p className={'text-xs mt-2'}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faHdd}
|
icon={faHdd}
|
||||||
@ -121,7 +124,7 @@ export default () => {
|
|||||||
className={'mr-1'}
|
className={'mr-1'}
|
||||||
/>
|
/>
|
||||||
{bytesToHuman(disk)}
|
{bytesToHuman(disk)}
|
||||||
<span className={'text-neutral-500'}> / {bytesToHuman(server.limits.disk * 1000 * 1000)}</span>
|
<span className={'text-neutral-500'}> / {disklimit}</span>
|
||||||
</p>
|
</p>
|
||||||
</TitledGreyBox>
|
</TitledGreyBox>
|
||||||
{!server.isInstalling ?
|
{!server.isInstalling ?
|
||||||
|
@ -12,7 +12,7 @@ import { ServerContext } from '@/state/server';
|
|||||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { uuid } = useServer();
|
const { uuid, featureLimits } = useServer();
|
||||||
const { addError, clearFlashes } = useFlash();
|
const { addError, clearFlashes } = useFlash();
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
|
|
||||||
@ -50,10 +50,22 @@ export default () => {
|
|||||||
/>)}
|
/>)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
{featureLimits.backups === 0 &&
|
||||||
|
<p className="text-center text-sm text-neutral-400">
|
||||||
|
Backups cannot be created for this server.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
<Can action={'backup.create'}>
|
<Can action={'backup.create'}>
|
||||||
|
{(featureLimits.backups > 0 && backups.length > 0) &&
|
||||||
|
<p className="text-center text-xs text-neutral-400 mt-2">
|
||||||
|
{backups.length} of {featureLimits.backups} backups have been created for this server.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
{featureLimits.backups > 0 && featureLimits.backups !== backups.length &&
|
||||||
<div className={'mt-6 flex justify-end'}>
|
<div className={'mt-6 flex justify-end'}>
|
||||||
<CreateBackupButton/>
|
<CreateBackupButton/>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</Can>
|
</Can>
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
);
|
);
|
||||||
|
@ -59,7 +59,12 @@ export default () => {
|
|||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<Can action={'database.create'}>
|
<Can action={'database.create'}>
|
||||||
{featureLimits.databases > 0 &&
|
{(featureLimits.databases > 0 && databases.length > 0) &&
|
||||||
|
<p className="text-center text-xs text-neutral-400 mt-2">
|
||||||
|
{databases.length} of {featureLimits.databases} databases have been allocated to this server.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
{featureLimits.databases > 0 && featureLimits.databases !== databases.length &&
|
||||||
<div className={'mt-6 flex justify-end'}>
|
<div className={'mt-6 flex justify-end'}>
|
||||||
<CreateDatabaseButton/>
|
<CreateDatabaseButton/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,7 +110,7 @@ export default () => {
|
|||||||
fetchContent={value => {
|
fetchContent={value => {
|
||||||
fetchFileContent = value;
|
fetchFileContent = value;
|
||||||
}}
|
}}
|
||||||
onContentSaved={() => null}
|
onContentSaved={() => save()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex justify-end mt-4'}>
|
<div className={'flex justify-end mt-4'}>
|
||||||
|
@ -25,10 +25,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
|||||||
.filter(directory => !!directory)
|
.filter(directory => !!directory)
|
||||||
.map((directory, index, dirs) => {
|
.map((directory, index, dirs) => {
|
||||||
if (!withinFileEditor && index === dirs.length - 1) {
|
if (!withinFileEditor && index === dirs.length - 1) {
|
||||||
return { name: directory };
|
return { name: decodeURIComponent(directory) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
|
return { name: decodeURIComponent(directory), path: `/${dirs.slice(0, index + 1).join('/')}` };
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -57,7 +57,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
|||||||
}
|
}
|
||||||
{file &&
|
{file &&
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<span className={'px-1 text-neutral-300'}>{file}</span>
|
<span className={'px-1 text-neutral-300'}>{decodeURIComponent(file)}</span>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,7 +70,12 @@ export default () => {
|
|||||||
/>
|
/>
|
||||||
<p className={'text-xs mt-2 text-neutral-400'}>
|
<p className={'text-xs mt-2 text-neutral-400'}>
|
||||||
<span className={'text-neutral-200'}>This directory will be created as</span>
|
<span className={'text-neutral-200'}>This directory will be created as</span>
|
||||||
/home/container/<span className={'text-cyan-200'}>{join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')}</span>
|
/home/container/
|
||||||
|
<span className={'text-cyan-200'}>
|
||||||
|
{decodeURIComponent(
|
||||||
|
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div className={'flex justify-end'}>
|
<div className={'flex justify-end'}>
|
||||||
<button className={'btn btn-sm btn-primary mt-8'}>
|
<button className={'btn btn-sm btn-primary mt-8'}>
|
||||||
|
@ -14,12 +14,26 @@ import Can from '@/components/elements/Can';
|
|||||||
import useServer from '@/plugins/useServer';
|
import useServer from '@/plugins/useServer';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
|
import { faFileArchive } from '@fortawesome/free-solid-svg-icons/faFileArchive';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
schedule: Schedule;
|
schedule: Schedule;
|
||||||
task: Task;
|
task: Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getActionDetails = (action: string): [ string, any ] => {
|
||||||
|
switch (action) {
|
||||||
|
case 'command':
|
||||||
|
return ['Send Command', faCode];
|
||||||
|
case 'power':
|
||||||
|
return ['Send Power Action', faToggleOn];
|
||||||
|
case 'backup':
|
||||||
|
return ['Create Backup', faFileArchive];
|
||||||
|
default:
|
||||||
|
return ['Unknown Action', faCode];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default ({ schedule, task }: Props) => {
|
export default ({ schedule, task }: Props) => {
|
||||||
const { uuid } = useServer();
|
const { uuid } = useServer();
|
||||||
const { clearFlashes, addError } = useFlash();
|
const { clearFlashes, addError } = useFlash();
|
||||||
@ -43,6 +57,8 @@ export default ({ schedule, task }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [ title, icon ] = getActionDetails(task.action);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}>
|
<div className={'flex items-center bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}>
|
||||||
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
|
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
|
||||||
@ -56,14 +72,19 @@ export default ({ schedule, task }: Props) => {
|
|||||||
onDismissed={() => setVisible(false)}
|
onDismissed={() => setVisible(false)}
|
||||||
onConfirmed={() => onConfirmDeletion()}
|
onConfirmed={() => onConfirmDeletion()}
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon icon={task.action === 'command' ? faCode : faToggleOn} className={'text-lg text-white'}/>
|
<FontAwesomeIcon icon={icon} className={'text-lg text-white'}/>
|
||||||
<div className={'flex-1'}>
|
<div className={'flex-1'}>
|
||||||
<p className={'ml-6 text-neutral-300 mb-2 uppercase text-xs'}>
|
<p className={'ml-6 text-neutral-300 uppercase text-xs'}>
|
||||||
{task.action === 'command' ? 'Send command' : 'Send power action'}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
<code className={'ml-6 font-mono bg-neutral-800 rounded py-1 px-2 text-sm'}>
|
{task.payload &&
|
||||||
{task.payload}
|
<div className={'ml-6 mt-2'}>
|
||||||
</code>
|
{task.action === 'backup' && <p className={'text-xs uppercase text-neutral-400 mb-1'}>Ignoring files & folders:</p>}
|
||||||
|
<div className={'font-mono bg-neutral-800 rounded py-1 px-2 text-sm w-auto whitespace-pre inline-block'}>
|
||||||
|
{task.payload}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{task.sequenceId > 1 &&
|
{task.sequenceId > 1 &&
|
||||||
<div className={'mr-6'}>
|
<div className={'mr-6'}>
|
||||||
|
@ -71,7 +71,10 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
|||||||
:
|
:
|
||||||
<div>
|
<div>
|
||||||
<label className={'input-dark-label'}>Ignored Files</label>
|
<label className={'input-dark-label'}>Ignored Files</label>
|
||||||
<FormikFieldWrapper name={'payload'}>
|
<FormikFieldWrapper
|
||||||
|
name={'payload'}
|
||||||
|
description={'Optional. Include the files and folders to be excluded in this backup. By default, the contents of your .pteroignore file will be used.'}
|
||||||
|
>
|
||||||
<FormikField as={'textarea'} name={'payload'} className={'input-dark h-32'}/>
|
<FormikField as={'textarea'} name={'payload'} className={'input-dark h-32'}/>
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +48,7 @@ const files: ServerFileStore = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
setDirectory: action((state, payload) => {
|
setDirectory: action((state, payload) => {
|
||||||
state.directory = cleanDirectoryPath(payload)
|
state.directory = cleanDirectoryPath(payload);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,6 +123,7 @@ select.input:not(.appearance-none) {
|
|||||||
select.input-dark:not(.appearance-none) {
|
select.input-dark:not(.appearance-none) {
|
||||||
@apply .bg-neutral-600 .border-neutral-500 .text-neutral-200;
|
@apply .bg-neutral-600 .border-neutral-500 .text-neutral-200;
|
||||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='%23C3D1DF' d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z'/%3e%3c/svg%3e ");
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='%23C3D1DF' d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z'/%3e%3c/svg%3e ");
|
||||||
|
background-color: hsl(220deg 21% 16%);
|
||||||
|
|
||||||
&:hover:not(:disabled), &:focus {
|
&:hover:not(:disabled), &:focus {
|
||||||
@apply .border-neutral-400;
|
@apply .border-neutral-400;
|
||||||
|
@ -27,3 +27,39 @@ code.clean {
|
|||||||
@apply .mt-4;
|
@apply .mt-4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
background: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border: solid 0 rgb(0 0 0 / 0%);
|
||||||
|
border-right-width: 4px;
|
||||||
|
border-left-width: 4px;
|
||||||
|
-webkit-border-radius: 9px 4px;
|
||||||
|
-webkit-box-shadow: inset 0 0 0 1px hsl(211, 10%, 53%), inset 0 0 0 4px hsl(209deg 18% 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track-piece {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:horizontal {
|
||||||
|
border-right-width: 0;
|
||||||
|
border-left-width: 0;
|
||||||
|
border-top-width: 4px;
|
||||||
|
border-bottom-width: 4px;
|
||||||
|
-webkit-border-radius: 4px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
-webkit-box-shadow:
|
||||||
|
inset 0 0 0 1px hsl(212, 92%, 43%),
|
||||||
|
inset 0 0 0 4px hsl(212, 92%, 43%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
swal({
|
swal({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Token created.',
|
title: 'Token created.',
|
||||||
text: '<p>To auto-configure your node run the following command:<br /><small><pre>cd /etc/pterodactyl && ./wings configure --panel-url {{ config('app.url') }} --token ' + data.token + ' --node ' + data.node + '{{ config('app.debug') ? ' --allow-insecure' : '' }}</pre></small></p>',
|
text: '<p>To auto-configure your node run the following command:<br /><small><pre>cd /etc/pterodactyl && sudo ./wings configure --panel-url {{ config('app.url') }} --token ' + data.token + ' --node ' + data.node + '{{ config('app.debug') ? ' --allow-insecure' : '' }}</pre></small></p>',
|
||||||
html: true
|
html: true
|
||||||
})
|
})
|
||||||
}).fail(function () {
|
}).fail(function () {
|
||||||
|
@ -176,6 +176,8 @@
|
|||||||
<input type="text" id="pMemory" name="memory" class="form-control" value="{{ old('memory') }}" />
|
<input type="text" id="pMemory" name="memory" class="form-control" value="{{ old('memory') }}" />
|
||||||
<span class="input-group-addon">MB</span>
|
<span class="input-group-addon">MB</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted small">The maximum amount of memory allowed for this container. Setting this to <code>0</code> will allow unlimited memory in a container.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group col-xs-6">
|
<div class="form-group col-xs-6">
|
||||||
@ -185,21 +187,18 @@
|
|||||||
<input type="text" id="pSwap" name="swap" class="form-control" value="{{ old('swap', 0) }}" />
|
<input type="text" id="pSwap" name="swap" class="form-control" value="{{ old('swap', 0) }}" />
|
||||||
<span class="input-group-addon">MB</span>
|
<span class="input-group-addon">MB</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-muted small">Setting this to <code>0</code> will disable swap space on this server. Setting to <code>-1</code> will allow unlimited swap.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box-footer no-border no-pad-top no-pad-bottom">
|
|
||||||
<p class="text-muted small">If you do not want to assign swap space to a server, simply put <code>0</code> for the value, or <code>-1</code> to allow unlimited swap space. If you want to disable memory limiting on a server, simply enter <code>0</code> into the memory field.<p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box-body row">
|
<div class="box-body row">
|
||||||
<div class="form-group col-xs-6">
|
<div class="form-group col-xs-6">
|
||||||
<label for="pDisk">Disk Space</label>
|
<label for="pDisk">Disk Space</label>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" id="pDisk" name="disk" class="form-control" value="{{ old('disk') }}" />
|
<input type="text" id="pDisk" name="disk" class="form-control" value="{{ old('disk') }}" />
|
||||||
<span class="input-group-addon">MB</span>
|
<span class="input-group-addon">MB</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-muted small">This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to <code>0</code> to allow unlimited disk usage.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group col-xs-6">
|
<div class="form-group col-xs-6">
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user