Merge branch 'feature/vuejs' into feature/vue-serverview

This commit is contained in:
Dane Everitt 2018-07-15 16:50:11 -07:00
commit f2d2725ca0
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
130 changed files with 2275 additions and 1363 deletions

View file

@ -20,7 +20,7 @@ MAIL_FROM_NAME="Pterodactyl Panel"
RECAPTCHA_ENABLED=false RECAPTCHA_ENABLED=false
DB_CONNECTION=testing DB_CONNECTION=testing
TESTING_DB_HOST=services.pterodactyl.local TESTING_DB_HOST=192.168.1.202
TESTING_DB_DATABASE=panel_test TESTING_DB_DATABASE=panel_test
TESTING_DB_USERNAME=panel_test TESTING_DB_USERNAME=panel_test
TESTING_DB_PASSWORD=Test1234 TESTING_DB_PASSWORD=Test1234

View file

@ -1,26 +0,0 @@
<!---
Please take a little time to submit a good issue. It makes our life easier and the issue will be resolved quicker.
!!! GitHub is NOT the place for difficulties setting up this software. Please use it for bugs and feature requests only. If you have issues setting up the panel or the daemon visit our Discord server: https://pterodactyl.io/discord
If you are submitting a feature request please remove everything in here. Then give a detailed explanation what you want to have implemented and why that would be a good addition.
Please also try to give the issue a good title: It should summarize your issue in a few words and help us see what the issue is about in a glance. Things like "Panel is not working" do not help.
--- You can delete everything above this line. --->
<!--- Please fill in the following basic information --->
* Panel or Daemon:
* Version of Panel/Daemon:
* Server's OS:
* Your Computer's OS & Browser:
------------------------
<!---
Please provide as much information about your issue as needed. Include precise steps to reproduce the issue and provide logs of the components that didn't work as expected.
Please provide additional information, depending on what you have issues with:
Panel: `php -v` (the php version in use).
Daemon: `uname -a` and `docker info` (your kernel version and information regarding docker)
--->

27
.github/ISSUE_TEMPLATE/---bug-report.md vendored Normal file
View file

@ -0,0 +1,27 @@
---
name: "\U0001F41B Bug Report"
about: Create a report to help us resolve a bug or error
---
**Background (please complete the following information):**
* Panel or Daemon:
* Version of Panel/Daemon:
* Server's OS:
* Your Computer's OS & Browser:
**Describe the bug**
A clear and concise description of what the bug is.
Please provide additional information too, depending on what you have issues with:
Panel: `php -v` (the php version in use).
Daemon: `uname -a` and `docker info` (your kernel version and information regarding docker)
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen. If applicable, add screenshots or a recording to help explain your problem.

View file

@ -0,0 +1,17 @@
---
name: "\U0001F680 Feature Request"
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -0,0 +1,8 @@
---
name: "⛔ Installation Help"
about: 'Visit our Discord for installation help: https://pterodactyl.io/discord'
---
We use GitHub issues only to discuss about Pterodactyl bugs and new features. For
this kind of questions about using Pterodactyl, please visit our Discord for assistance: https://pterodactyl.io/discord

View file

@ -1,5 +1,17 @@
language: php language: php
dist: trusty dist: trusty
git:
depth: 3
quiet: true
matrix:
fast_finish: true
allow_failures:
- env: TEST_SUITE=Coverage
env:
matrix:
- TEST_SUITE=Unit
- TEST_SUITE=Coverage
- TEST_SUITE=Integration
php: php:
- 7.2 - 7.2
sudo: false sudo: false
@ -15,8 +27,9 @@ before_script:
- cp .env.travis .env - cp .env.travis .env
- travis_retry composer install --no-interaction --prefer-dist --no-suggest - travis_retry composer install --no-interaction --prefer-dist --no-suggest
script: script:
- vendor/bin/phpunit --bootstrap vendor/autoload.php --coverage-clover coverage.xml tests/Unit - if [ "$TEST_SUITE" = "Unit" ]; then vendor/bin/phpunit --bootstrap vendor/autoload.php tests/Unit; fi;
- vendor/bin/phpunit tests/Integration - if [ "$TEST_SUITE" = "Coverage" ]; then vendor/bin/phpunit --bootstrap vendor/autoload.php --coverage-clover coverage.xml tests/Unit; fi;
- if [ "$TEST_SUITE" = "Integration" ]; then vendor/bin/phpunit tests/Integration; fi;
notifications: notifications:
email: false email: false
webhooks: webhooks:

View file

@ -3,17 +3,30 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines. This project follows [Semantic Versioning](http://semver.org) guidelines.
## v0.7.9 (Derelict Dermodactylus)
### Fixed
* Fixes a two-factor authentication bypass present in the password reset process for an account.
## v0.7.8 (Derelict Dermodactylus) ## v0.7.8 (Derelict Dermodactylus)
### Added ### Added
* Nodes can now be put into maintenance mode to deny access to servers temporarily. * Nodes can now be put into maintenance mode to deny access to servers temporarily.
* Basic statistics about your panel are now available in the Admin CP. * Basic statistics about your panel are now available in the Admin CP.
* Added support for using a MySQL socket location for connections rather than a TCP connection. Set a `DB_SOCKET` variable in your `.env` file to use this.
### Fixed ### Fixed
* Hitting Ctrl+Z when editing a file on the web now works as expected. * Hitting Ctrl+Z when editing a file on the web now works as expected.
* Logo now links to the correct location on all pages. * Logo now links to the correct location on all pages.
* Permissions checking to determine if a user can see the task management page now works correctly.
* Fixed `pterodactyl.environment_variables` to be used correctly for global environment variables. The wrong config variable name was being using previously.
* Fixes tokens being sent to users when their account is created to actually work. Implements Laravel's internal token creation mechanisms rather than trying to do it custom.
* Updates some eggs to ensure they have the correct data and will continue working down the road. Fixes autoupdating on some source servers and MC related download links.
* Emails should send properly now when a server is marked as installed to let the owner know it is ready for action.
* Cancelling a file manager operation should cancel correctly across all browsers now.
### Changed ### Changed
* Attempting to upload a folder via the web file manager will now display a warning telling the user to use SFTP. * Attempting to upload a folder via the web file manager will now display a warning telling the user to use SFTP.
* Changing your account password will now log out all other sessions that currently exist for that user.
* Subusers with no permissions selected can be created.
## v0.7.7 (Derelict Dermodactylus) ## v0.7.7 (Derelict Dermodactylus)
### Fixed ### Fixed

View file

@ -0,0 +1,15 @@
<?php
namespace Pterodactyl\Contracts\Core;
use Pterodactyl\Events\Event;
interface ReceivesEvents
{
/**
* Handles receiving an event from the application.
*
* @param \Pterodactyl\Events\Event $notification
*/
public function handle(Event $notification): void;
}

View file

@ -202,7 +202,7 @@ interface RepositoryInterface
public function insertIgnore(array $values): bool; public function insertIgnore(array $values): bool;
/** /**
* Get the amount of entries in the database * Get the amount of entries in the database.
* *
* @return int * @return int
*/ */

View file

@ -147,7 +147,7 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
public function isUniqueUuidCombo(string $uuid, string $short): bool; public function isUniqueUuidCombo(string $uuid, string $short): bool;
/** /**
* Get the amount of servers that are suspended * Get the amount of servers that are suspended.
* *
* @return int * @return int
*/ */

View file

@ -0,0 +1,27 @@
<?php
namespace Pterodactyl\Events\Server;
use Pterodactyl\Events\Event;
use Pterodactyl\Models\Server;
use Illuminate\Queue\SerializesModels;
class Installed extends Event
{
use SerializesModels;
/**
* @var \Pterodactyl\Models\Server
*/
public $server;
/**
* Create a new event instance.
*
* @var \Pterodactyl\Models\Server
*/
public function __construct(Server $server)
{
$this->server = $server;
}
}

View file

@ -231,7 +231,7 @@ class Handler extends ExceptionHandler
protected function unauthenticated($request, AuthenticationException $exception) protected function unauthenticated($request, AuthenticationException $exception)
{ {
if ($request->expectsJson()) { if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401); return response()->json(self::convertToArray($exception), 401);
} }
return redirect()->guest(route('auth.login')); return redirect()->guest(route('auth.login'));

View file

@ -2,16 +2,14 @@
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Traits\Controllers\PlainJavascriptInjection; use Pterodactyl\Traits\Controllers\PlainJavascriptInjection;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
class StatisticsController extends Controller class StatisticsController extends Controller
{ {
@ -29,15 +27,14 @@ class StatisticsController extends Controller
private $userRepository; private $userRepository;
function __construct( public function __construct(
AllocationRepositoryInterface $allocationRepository, AllocationRepositoryInterface $allocationRepository,
DatabaseRepositoryInterface $databaseRepository, DatabaseRepositoryInterface $databaseRepository,
EggRepositoryInterface $eggRepository, EggRepositoryInterface $eggRepository,
NodeRepositoryInterface $nodeRepository, NodeRepositoryInterface $nodeRepository,
ServerRepositoryInterface $serverRepository, ServerRepositoryInterface $serverRepository,
UserRepositoryInterface $userRepository UserRepositoryInterface $userRepository
) ) {
{
$this->allocationRepository = $allocationRepository; $this->allocationRepository = $allocationRepository;
$this->databaseRepository = $databaseRepository; $this->databaseRepository = $databaseRepository;
$this->eggRepository = $eggRepository; $this->eggRepository = $eggRepository;
@ -83,7 +80,7 @@ class StatisticsController extends Controller
'nodes' => $nodes, 'nodes' => $nodes,
'tokens' => $tokens, 'tokens' => $tokens,
]); ]);
return view('admin.statistics', [ return view('admin.statistics', [
'servers' => $servers, 'servers' => $servers,
'nodes' => $nodes, 'nodes' => $nodes,
@ -97,5 +94,4 @@ class StatisticsController extends Controller
'totalAllocations' => $totalAllocations, 'totalAllocations' => $totalAllocations,
]); ]);
} }
} }

View file

@ -3,14 +3,71 @@
namespace Pterodactyl\Http\Controllers\Api\Client; namespace Pterodactyl\Http\Controllers\Api\Client;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Transformers\Api\Client\AccountTransformer; use Pterodactyl\Transformers\Api\Client\AccountTransformer;
use Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest;
use Pterodactyl\Http\Requests\Api\Client\Account\UpdatePasswordRequest;
class AccountController extends ClientApiController class AccountController extends ClientApiController
{ {
/**
* @var \Pterodactyl\Services\Users\UserUpdateService
*/
private $updateService;
/**
* AccountController constructor.
*
* @param \Pterodactyl\Services\Users\UserUpdateService $updateService
*/
public function __construct(UserUpdateService $updateService)
{
parent::__construct();
$this->updateService = $updateService;
}
/**
* @param Request $request
* @return array
*/
public function index(Request $request): array public function index(Request $request): array
{ {
return $this->fractal->item($request->user()) return $this->fractal->item($request->user())
->transformWith($this->getTransformer(AccountTransformer::class)) ->transformWith($this->getTransformer(AccountTransformer::class))
->toArray(); ->toArray();
} }
/**
* Update the authenticated user's email address.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest $request
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function updateEmail(UpdateEmailRequest $request): Response
{
$this->updateService->handle($request->user(), $request->validated());
return response('', Response::HTTP_CREATED);
}
/**
* Update the authenticated user's password.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdatePasswordRequest $request
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function updatePassword(UpdatePasswordRequest $request): Response
{
$this->updateService->handle($request->user(), $request->validated());
return response('', Response::HTTP_CREATED);
}
} }

View file

@ -2,8 +2,6 @@
namespace Pterodactyl\Http\Controllers\Auth; namespace Pterodactyl\Http\Controllers\Auth;
use Cake\Chronos\Chronos;
use Lcobucci\JWT\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager; use Illuminate\Auth\AuthManager;
@ -15,25 +13,18 @@ use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Pterodactyl\Traits\Helpers\ProvidesJWTServices;
use Pterodactyl\Transformers\Api\Client\AccountTransformer;
use Illuminate\Contracts\Cache\Repository as CacheRepository; use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
abstract class AbstractLoginController extends Controller abstract class AbstractLoginController extends Controller
{ {
use AuthenticatesUsers, ProvidesJWTServices; use AuthenticatesUsers;
/** /**
* @var \Illuminate\Auth\AuthManager * @var \Illuminate\Auth\AuthManager
*/ */
protected $auth; protected $auth;
/**
* @var \Lcobucci\JWT\Builder
*/
protected $builder;
/** /**
* @var \Illuminate\Contracts\Cache\Repository * @var \Illuminate\Contracts\Cache\Repository
*/ */
@ -79,7 +70,6 @@ abstract class AbstractLoginController extends Controller
* LoginController constructor. * LoginController constructor.
* *
* @param \Illuminate\Auth\AuthManager $auth * @param \Illuminate\Auth\AuthManager $auth
* @param \Lcobucci\JWT\Builder $builder
* @param \Illuminate\Contracts\Cache\Repository $cache * @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \PragmaRX\Google2FA\Google2FA $google2FA * @param \PragmaRX\Google2FA\Google2FA $google2FA
@ -87,14 +77,12 @@ abstract class AbstractLoginController extends Controller
*/ */
public function __construct( public function __construct(
AuthManager $auth, AuthManager $auth,
Builder $builder,
CacheRepository $cache, CacheRepository $cache,
Encrypter $encrypter, Encrypter $encrypter,
Google2FA $google2FA, Google2FA $google2FA,
UserRepositoryInterface $repository UserRepositoryInterface $repository
) { ) {
$this->auth = $auth; $this->auth = $auth;
$this->builder = $builder;
$this->cache = $cache; $this->cache = $cache;
$this->encrypter = $encrypter; $this->encrypter = $encrypter;
$this->google2FA = $google2FA; $this->google2FA = $google2FA;
@ -143,32 +131,10 @@ abstract class AbstractLoginController extends Controller
return response()->json([ return response()->json([
'complete' => true, 'complete' => true,
'intended' => $this->redirectPath(), 'intended' => $this->redirectPath(),
'jwt' => $this->createJsonWebToken($user), 'user' => $user->toVueObject(),
]); ]);
} }
/**
* Create a new JWT for the request and sign it using the signing key.
*
* @param User $user
* @return string
*/
protected function createJsonWebToken(User $user): string
{
$token = $this->builder
->setIssuer('Pterodactyl Panel')
->setAudience(config('app.url'))
->setId(str_random(16), true)
->setIssuedAt(Chronos::now()->getTimestamp())
->setNotBefore(Chronos::now()->getTimestamp())
->setExpiration(Chronos::now()->addSeconds(config('session.lifetime'))->getTimestamp())
->set('user', (new AccountTransformer())->transform($user))
->sign($this->getJWTSigner(), $this->getJWTSigningKey())
->getToken();
return $token->__toString();
}
/** /**
* Determine if the user is logging in using an email or username,. * Determine if the user is logging in using an email or username,.
* *

View file

@ -2,12 +2,17 @@
namespace Pterodactyl\Http\Controllers\Auth; namespace Pterodactyl\Http\Controllers\Auth;
use Illuminate\Support\Str;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Contracts\Events\Dispatcher;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Foundation\Auth\ResetsPasswords;
use Pterodactyl\Http\Requests\Auth\ResetPasswordRequest; use Pterodactyl\Http\Requests\Auth\ResetPasswordRequest;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class ResetPasswordController extends Controller class ResetPasswordController extends Controller
{ {
@ -20,6 +25,40 @@ class ResetPasswordController extends Controller
*/ */
public $redirectTo = '/'; public $redirectTo = '/';
/**
* @var bool
*/
protected $hasTwoFactor = false;
/**
* @var \Illuminate\Contracts\Events\Dispatcher
*/
private $dispatcher;
/**
* @var \Illuminate\Contracts\Hashing\Hasher
*/
private $hasher;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/
private $userRepository;
/**
* ResetPasswordController constructor.
*
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @param \Illuminate\Contracts\Hashing\Hasher $hasher
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository
*/
public function __construct(Dispatcher $dispatcher, Hasher $hasher, UserRepositoryInterface $userRepository)
{
$this->dispatcher = $dispatcher;
$this->hasher = $hasher;
$this->userRepository = $userRepository;
}
/** /**
* Reset the given user's password. * Reset the given user's password.
* *
@ -49,6 +88,35 @@ class ResetPasswordController extends Controller
throw new DisplayException(trans($response)); throw new DisplayException(trans($response));
} }
/**
* Reset the given user's password. If the user has two-factor authentication enabled on their
* account do not automatically log them in. In those cases, send the user back to the login
* form with a note telling them their password was changed and to log back in.
*
* @param \Illuminate\Contracts\Auth\CanResetPassword|\Pterodactyl\Models\User $user
* @param string $password
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
protected function resetPassword($user, $password)
{
$user = $this->userRepository->update($user->id, [
'password' => $this->hasher->make($password),
$user->getRememberTokenName() => Str::random(60),
]);
$this->dispatcher->dispatch(new PasswordReset($user));
// If the user is not using 2FA log them in, otherwise skip this step and force a
// fresh login where they'll be prompted to enter a token.
if (! $user->use_totp) {
$this->guard()->login($user);
}
$this->hasTwoFactor = $user->use_totp;
}
/** /**
* Send a successful password reset response back to the callee. * Send a successful password reset response back to the callee.
* *
@ -59,6 +127,7 @@ class ResetPasswordController extends Controller
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'redirect_to' => $this->redirectTo, 'redirect_to' => $this->redirectTo,
'send_to_login' => $this->hasTwoFactor,
]); ]);
} }
} }

View file

@ -1,72 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Base;
use Pterodactyl\Models\User;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Http\Requests\Base\AccountDataFormRequest;
class AccountController extends Controller
{
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
protected $alert;
/**
* @var \Pterodactyl\Services\Users\UserUpdateService
*/
protected $updateService;
/**
* AccountController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Services\Users\UserUpdateService $updateService
*/
public function __construct(AlertsMessageBag $alert, UserUpdateService $updateService)
{
$this->alert = $alert;
$this->updateService = $updateService;
}
/**
* Display base account information page.
*
* @return \Illuminate\View\View
*/
public function index()
{
return view('base.account');
}
/**
* Update details for a user's account.
*
* @param \Pterodactyl\Http\Requests\Base\AccountDataFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function update(AccountDataFormRequest $request)
{
$data = [];
if ($request->input('do_action') === 'password') {
$data['password'] = $request->input('new_password');
} elseif ($request->input('do_action') === 'email') {
$data['email'] = $request->input('new_email');
} elseif ($request->input('do_action') === 'identity') {
$data = $request->only(['name_first', 'name_last', 'username']);
}
$this->updateService->setUserLevel(User::USER_LEVEL_USER);
$this->updateService->handle($request->user(), $data);
$this->alert->success(trans('base.account.details_updated'))->flash();
return redirect()->route('account');
}
}

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Controllers\Base; namespace Pterodactyl\Http\Controllers\Base;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Users\TwoFactorSetupService; use Pterodactyl\Services\Users\TwoFactorSetupService;
@ -62,36 +63,28 @@ class SecurityController extends Controller
} }
/** /**
* Returns Security Management Page. * Return information about the user's two-factor authentication status. If not enabled setup their
* * secret and return information to allow the user to proceede with setup.
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function index(Request $request)
{
if ($this->config->get('session.driver') === 'database') {
$activeSessions = $this->repository->getUserSessions($request->user()->id);
}
return view('base.security', [
'sessions' => $activeSessions ?? null,
]);
}
/**
* Generates TOTP Secret and returns popup data for user to verify
* that they can generate a valid response.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse * @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 generateTotp(Request $request) public function index(Request $request): JsonResponse
{ {
return response()->json([ if ($request->user()->use_totp) {
'qrImage' => $this->twoFactorSetupService->handle($request->user()), return JsonResponse::create([
'enabled' => true,
]);
}
$response = $this->twoFactorSetupService->handle($request->user());
return JsonResponse::create([
'enabled' => false,
'qr_image' => $response->get('image'),
'secret' => $response->get('secret'),
]); ]);
} }
@ -99,53 +92,43 @@ class SecurityController extends Controller
* Verifies that 2FA token received is valid and will work on the account. * Verifies that 2FA token received is valid and will work on the account.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $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 setTotp(Request $request) public function store(Request $request): JsonResponse
{ {
try { try {
$this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? ''); $this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? '');
return response('true');
} catch (TwoFactorAuthenticationTokenInvalid $exception) { } catch (TwoFactorAuthenticationTokenInvalid $exception) {
return response('false'); $error = true;
} }
return JsonResponse::create([
'success' => ! isset($error),
]);
} }
/** /**
* Disables TOTP on an account. * Disables TOTP on an account.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse * @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 disableTotp(Request $request) public function delete(Request $request): JsonResponse
{ {
try { try {
$this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? '', false); $this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? '', false);
} catch (TwoFactorAuthenticationTokenInvalid $exception) { } catch (TwoFactorAuthenticationTokenInvalid $exception) {
$this->alert->danger(trans('base.security.2fa_disable_error'))->flash(); $error = true;
} }
return redirect()->route('account.security'); return JsonResponse::create([
} 'success' => ! isset($error),
]);
/**
* Revokes a user session.
*
* @param \Illuminate\Http\Request $request
* @param string $id
* @return \Illuminate\Http\RedirectResponse
*/
public function revoke(Request $request, string $id)
{
$this->repository->deleteUserSession($request->user()->id, $id);
return redirect()->route('account.security');
} }
} }

View file

@ -7,9 +7,26 @@ use Illuminate\Http\Request;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Events\Server\Installed as ServerInstalled;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
class ActionController extends Controller class ActionController extends Controller
{ {
/**
* @var \Illuminate\Contracts\Events\Dispatcher
*/
private $eventDispatcher;
/**
* ActionController constructor.
*
* @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher
*/
public function __construct(EventDispatcher $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/** /**
* Handles install toggle request from daemon. * Handles install toggle request from daemon.
* *
@ -37,6 +54,11 @@ class ActionController extends Controller
$server->installed = ($status === 'installed') ? 1 : 2; $server->installed = ($status === 'installed') ? 1 : 2;
$server->save(); $server->save();
// Only fire event if server installed successfully.
if ($server->installed === 1) {
$this->eventDispatcher->dispatch(new ServerInstalled($server));
}
return response()->json([]); return response()->json([]);
} }

View file

@ -157,7 +157,6 @@ class SubuserController extends Controller
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Exception * @throws \Exception
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException * @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException
@ -171,7 +170,7 @@ class SubuserController extends Controller
$this->alert->success(trans('server.users.user_assigned'))->flash(); $this->alert->success(trans('server.users.user_assigned'))->flash();
return redirect()->route('server.subusers.view', [ return redirect()->route('server.subusers.view', [
'uuid' => $server->uuid, 'uuid' => $server->uuidShort,
'id' => $subuser->hashid, 'id' => $subuser->hashid,
]); ]);
} }

View file

@ -17,8 +17,8 @@ use Pterodactyl\Http\Middleware\LanguageMiddleware;
use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Pterodactyl\Http\Middleware\Api\AuthenticateKey; use Pterodactyl\Http\Middleware\Api\AuthenticateKey;
use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Routing\Middleware\SubstituteBindings;
use Pterodactyl\Http\Middleware\AccessingValidServer;
use Pterodactyl\Http\Middleware\Api\SetSessionDriver; use Pterodactyl\Http\Middleware\Api\SetSessionDriver;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\View\Middleware\ShareErrorsFromSession; use Illuminate\View\Middleware\ShareErrorsFromSession;
use Pterodactyl\Http\Middleware\MaintenanceMiddleware; use Pterodactyl\Http\Middleware\MaintenanceMiddleware;
use Pterodactyl\Http\Middleware\RedirectIfAuthenticated; use Pterodactyl\Http\Middleware\RedirectIfAuthenticated;
@ -27,6 +27,7 @@ use Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess;
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; 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\AuthenticateAsSubuser; 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\Server\SubuserBelongsToServer;
@ -48,6 +49,7 @@ class Kernel extends HttpKernel
*/ */
protected $middleware = [ protected $middleware = [
CheckForMaintenanceMode::class, CheckForMaintenanceMode::class,
EncryptCookies::class,
ValidatePostSize::class, ValidatePostSize::class,
TrimStrings::class, TrimStrings::class,
ConvertEmptyStringsToNull::class, ConvertEmptyStringsToNull::class,
@ -61,9 +63,9 @@ class Kernel extends HttpKernel
*/ */
protected $middlewareGroups = [ protected $middlewareGroups = [
'web' => [ 'web' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class, AddQueuedCookiesToResponse::class,
StartSession::class, StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class, ShareErrorsFromSession::class,
VerifyCsrfToken::class, VerifyCsrfToken::class,
SubstituteBindings::class, SubstituteBindings::class,
@ -80,8 +82,10 @@ class Kernel extends HttpKernel
], ],
'client-api' => [ 'client-api' => [
'throttle:240,1', 'throttle:240,1',
SubstituteClientApiBindings::class, StartSession::class,
SetSessionDriver::class, SetSessionDriver::class,
AuthenticateSession::class,
SubstituteClientApiBindings::class,
'api..key:' . ApiKey::TYPE_ACCOUNT, 'api..key:' . ApiKey::TYPE_ACCOUNT,
AuthenticateIPAccess::class, AuthenticateIPAccess::class,
], ],

View file

@ -3,13 +3,12 @@
namespace Pterodactyl\Http\Middleware\Api; namespace Pterodactyl\Http\Middleware\Api;
use Closure; use Closure;
use Lcobucci\JWT\Parser;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Pterodactyl\Models\ApiKey; use Pterodactyl\Models\ApiKey;
use Illuminate\Auth\AuthManager; use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Traits\Helpers\ProvidesJWTServices;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
@ -17,8 +16,6 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class AuthenticateKey class AuthenticateKey
{ {
use ProvidesJWTServices;
/** /**
* @var \Illuminate\Auth\AuthManager * @var \Illuminate\Auth\AuthManager
*/ */
@ -62,57 +59,29 @@ class AuthenticateKey
*/ */
public function handle(Request $request, Closure $next, int $keyType) public function handle(Request $request, Closure $next, int $keyType)
{ {
if (is_null($request->bearerToken())) { if (is_null($request->bearerToken()) && is_null($request->user())) {
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
} }
$raw = $request->bearerToken(); $raw = $request->bearerToken();
// This is an internal JWT, treat it differently to get the correct user before passing it along. // This is a request coming through using cookies, we have an authenticated user not using
if (strlen($raw) > ApiKey::IDENTIFIER_LENGTH + ApiKey::KEY_LENGTH) { // an API key. Make some fake API key models and continue on through the process.
$model = $this->authenticateJWT($raw); if (empty($raw) && $request->user() instanceof User) {
$model = (new ApiKey())->forceFill([
'user_id' => $request->user()->id,
'key_type' => ApiKey::TYPE_ACCOUNT,
]);
} else { } else {
$model = $this->authenticateApiKey($raw, $keyType); $model = $this->authenticateApiKey($raw, $keyType);
$this->auth->guard()->loginUsingId($model->user_id);
} }
$this->auth->guard()->loginUsingId($model->user_id);
$request->attributes->set('api_key', $model); $request->attributes->set('api_key', $model);
return $next($request); return $next($request);
} }
/**
* Authenticate an API request using a JWT rather than an API key.
*
* @param string $token
* @return \Pterodactyl\Models\ApiKey
*/
protected function authenticateJWT(string $token): ApiKey
{
$token = (new Parser)->parse($token);
// If the key cannot be verified throw an exception to indicate that a bad
// authorization header was provided.
if (! $token->verify($this->getJWTSigner(), $this->getJWTSigningKey())) {
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
}
// Run through the token validation and throw an exception if the token is not valid.
if (
$token->getClaim('nbf') > Chronos::now()->getTimestamp()
|| $token->getClaim('iss') !== 'Pterodactyl Panel'
|| $token->getClaim('aud') !== config('app.url')
|| $token->getClaim('exp') <= Chronos::now()->getTimestamp()
) {
throw new AccessDeniedHttpException;
}
return (new ApiKey)->forceFill([
'user_id' => object_get($token->getClaim('user'), 'id', 0),
'key_type' => ApiKey::TYPE_ACCOUNT,
]);
}
/** /**
* Authenticate an API key. * Authenticate an API key.
* *

View file

@ -4,16 +4,10 @@ namespace Pterodactyl\Http\Middleware\Api;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Config\Repository as ConfigRepository;
class SetSessionDriver class SetSessionDriver
{ {
/**
* @var \Illuminate\Contracts\Foundation\Application
*/
private $app;
/** /**
* @var \Illuminate\Contracts\Config\Repository * @var \Illuminate\Contracts\Config\Repository
*/ */
@ -22,12 +16,10 @@ class SetSessionDriver
/** /**
* SetSessionDriver constructor. * SetSessionDriver constructor.
* *
* @param \Illuminate\Contracts\Foundation\Application $app * @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Config\Repository $config
*/ */
public function __construct(Application $app, ConfigRepository $config) public function __construct(ConfigRepository $config)
{ {
$this->app = $app;
$this->config = $config; $this->config = $config;
} }

View file

@ -10,6 +10,7 @@
namespace Pterodactyl\Http\Middleware; namespace Pterodactyl\Http\Middleware;
use Closure; use Closure;
use Illuminate\Support\Str;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
@ -24,27 +25,12 @@ class RequireTwoFactorAuthentication
*/ */
private $alert; private $alert;
/**
* The names of routes that should be accessible without 2FA enabled.
*
* @var array
*/
protected $except = [
'account.security',
'account.security.revoke',
'account.security.totp',
'account.security.totp.set',
'account.security.totp.disable',
'auth.totp',
'auth.logout',
];
/** /**
* The route to redirect a user to to enable 2FA. * The route to redirect a user to to enable 2FA.
* *
* @var string * @var string
*/ */
protected $redirectRoute = 'account.security'; protected $redirectRoute = 'account';
/** /**
* RequireTwoFactorAuthentication constructor. * RequireTwoFactorAuthentication constructor.
@ -69,7 +55,8 @@ class RequireTwoFactorAuthentication
return $next($request); return $next($request);
} }
if (in_array($request->route()->getName(), $this->except)) { $current = $request->route()->getName();
if (in_array($current, ['auth', 'account']) || Str::startsWith($current, ['auth.', 'account.'])) {
return $next($request); return $next($request);
} }

View file

@ -1,11 +1,10 @@
<?php <?php
namespace Pterodactyl\Http\Middleware; namespace Pterodactyl\Http\Middleware\Server;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Contracts\Session\Session;
use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
@ -29,29 +28,21 @@ class AccessingValidServer
*/ */
private $response; private $response;
/**
* @var \Illuminate\Contracts\Session\Session
*/
private $session;
/** /**
* AccessingValidServer constructor. * AccessingValidServer constructor.
* *
* @param \Illuminate\Contracts\Config\Repository $config * @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Routing\ResponseFactory $response * @param \Illuminate\Contracts\Routing\ResponseFactory $response
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Illuminate\Contracts\Session\Session $session
*/ */
public function __construct( public function __construct(
ConfigRepository $config, ConfigRepository $config,
ResponseFactory $response, ResponseFactory $response,
ServerRepositoryInterface $repository, ServerRepositoryInterface $repository
Session $session
) { ) {
$this->config = $config; $this->config = $config;
$this->repository = $repository; $this->repository = $repository;
$this->response = $response; $this->response = $response;
$this->session = $session;
} }
/** /**
@ -61,7 +52,6 @@ class AccessingValidServer
* @param \Closure $next * @param \Closure $next
* @return \Illuminate\Http\Response|mixed * @return \Illuminate\Http\Response|mixed
* *
* @throws \Illuminate\Auth\AuthenticationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
@ -90,10 +80,6 @@ class AccessingValidServer
return $this->response->view('errors.installing', [], 409); return $this->response->view('errors.installing', [], 409);
} }
// Store the server in the session.
// @todo remove from session. use request attributes.
$this->session->now('server_data.model', $server);
// Add server to the request attributes. This will replace sessions // Add server to the request attributes. This will replace sessions
// as files are updated. // as files are updated.
$request->attributes->set('server', $server); $request->attributes->set('server', $server);

View file

@ -74,7 +74,7 @@ class StoreNodeRequest extends ApplicationApiRequest
$response = parent::validated(); $response = parent::validated();
$response['daemonListen'] = $response['daemon_listen']; $response['daemonListen'] = $response['daemon_listen'];
$response['daemonSFTP'] = $response['daemon_sftp']; $response['daemonSFTP'] = $response['daemon_sftp'];
$response['daemonBase'] = $response['daemon_base']; $response['daemonBase'] = $response['daemon_base'] ?? (new Node)->getAttribute('daemonBase');
unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']); unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']);

View file

@ -0,0 +1,39 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Account;
use Pterodactyl\Models\User;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException;
class UpdateEmailRequest extends ClientApiRequest
{
/**
* @return bool
*
* @throws \Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException
*/
public function authorize(): bool
{
if (! parent::authorize()) {
return false;
}
// Verify password matches when changing password or email.
if (! password_verify($this->input('password'), $this->user()->password)) {
throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password'));
}
return true;
}
/**
* @return array
*/
public function rules(): array
{
$rules = User::getUpdateRulesForId($this->user()->id);
return ['email' => $rules['email']];
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Account;
use Pterodactyl\Models\User;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException;
class UpdatePasswordRequest extends ClientApiRequest
{
/**
* @return bool
*
* @throws \Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException
*/
public function authorize(): bool
{
if (! parent::authorize()) {
return false;
}
// Verify password matches when changing password or email.
if (! password_verify($this->input('current_password'), $this->user()->password)) {
throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password'));
}
return true;
}
/**
* @return array
*/
public function rules(): array
{
$rules = User::getUpdateRulesForId($this->user()->id);
return ['password' => array_merge($rules['password'], ['confirmed'])];
}
}

View file

@ -28,7 +28,7 @@ class AccountDataFormRequest extends FrontendUserFormRequest
// Verify password matches when changing password or email. // Verify password matches when changing password or email.
if (in_array($this->input('do_action'), ['password', 'email'])) { if (in_array($this->input('do_action'), ['password', 'email'])) {
if (! password_verify($this->input('current_password'), $this->user()->password)) { if (! password_verify($this->input('current_password'), $this->user()->password)) {
throw new InvalidPasswordProvidedException(trans('base.account.invalid_password')); throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password'));
} }
} }

View file

@ -25,7 +25,7 @@ class SubuserStoreFormRequest extends ServerFormRequest
{ {
return [ return [
'email' => 'required|email', 'email' => 'required|email',
'permissions' => 'present|array', 'permissions' => 'sometimes|array',
]; ];
} }
} }

View file

View file

@ -5,6 +5,7 @@ namespace Pterodactyl\Models;
use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable; use Sofa\Eloquence\Validable;
use Pterodactyl\Rules\Username; use Pterodactyl\Rules\Username;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\In;
use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -177,6 +178,16 @@ class User extends Model implements
return $rules; return $rules;
} }
/**
* Return the user model in a format that can be passed over to Vue templates.
*
* @return array
*/
public function toVueObject(): array
{
return (new Collection($this->toArray()))->except(['id', 'external_id'])->toArray();
}
/** /**
* Send the password reset notification. * Send the password reset notification.
* *

View file

@ -23,7 +23,7 @@ class AccountCreated extends Notification implements ShouldQueue
/** /**
* The user model for the created user. * The user model for the created user.
* *
* @var object * @var \Pterodactyl\Models\User
*/ */
public $user; public $user;
@ -65,7 +65,7 @@ class AccountCreated extends Notification implements ShouldQueue
->line('Email: ' . $this->user->email); ->line('Email: ' . $this->user->email);
if (! is_null($this->token)) { if (! is_null($this->token)) {
return $message->action('Setup Your Account', url('/auth/password/reset/' . $this->token . '?email=' . $this->user->email)); return $message->action('Setup Your Account', url('/auth/password/reset/' . $this->token . '?email=' . urlencode($this->user->email)));
} }
return $message; return $message;

View file

@ -0,0 +1,69 @@
<?php
namespace Pterodactyl\Notifications;
use Illuminate\Bus\Queueable;
use Pterodactyl\Events\Event;
use Illuminate\Container\Container;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Pterodactyl\Contracts\Core\ReceivesEvents;
use Illuminate\Contracts\Notifications\Dispatcher;
use Illuminate\Notifications\Messages\MailMessage;
class ServerInstalled extends Notification implements ShouldQueue, ReceivesEvents
{
use Queueable;
/**
* @var \Pterodactyl\Models\Server
*/
public $server;
/**
* @var \Pterodactyl\Models\User
*/
public $user;
/**
* Handle a direct call to this notification from the server installed event. This is configured
* in the event service provider.
*
* @param \Pterodactyl\Events\Event|\Pterodactyl\Events\Server\Installed $event
*/
public function handle(Event $event): void
{
$event->server->loadMissing('user');
$this->server = $event->server;
$this->user = $event->server->user;
// Since we are calling this notification directly from an event listener we need to fire off the dispatcher
// to send the email now. Don't use send() or you'll end up firing off two different events.
Container::getInstance()->make(Dispatcher::class)->sendNow($this->user, $this);
}
/**
* Get the notification's delivery channels.
*
* @return array
*/
public function via()
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail()
{
return (new MailMessage)
->greeting('Hello ' . $this->user->username . '.')
->line('Your server has finished installing and is now ready for you to use.')
->line('Server Name: ' . $this->server->name)
->action('Login and Begin Using', route('index'));
}
}

View file

@ -2,6 +2,8 @@
namespace Pterodactyl\Providers; namespace Pterodactyl\Providers;
use Pterodactyl\Events\Server\Installed as ServerInstalledEvent;
use Pterodactyl\Notifications\ServerInstalled as ServerInstalledNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider class EventServiceProvider extends ServiceProvider
@ -11,5 +13,9 @@ class EventServiceProvider extends ServiceProvider
* *
* @var array * @var array
*/ */
protected $listen = []; protected $listen = [
ServerInstalledEvent::class => [
ServerInstalledNotification::class,
],
];
} }

View file

@ -144,6 +144,7 @@ abstract class BaseRepository implements BaseRepositoryInterface
$headers['X-Access-Token'] = $this->getToken() ?? $this->getNode()->daemonSecret; $headers['X-Access-Token'] = $this->getToken() ?? $this->getNode()->daemonSecret;
return new Client([ return new Client([
'verify' => config('app.env') === 'production',
'base_uri' => sprintf('%s://%s:%s/v1/', $this->getNode()->scheme, $this->getNode()->fqdn, $this->getNode()->daemonListen), 'base_uri' => sprintf('%s://%s:%s/v1/', $this->getNode()->scheme, $this->getNode()->fqdn, $this->getNode()->daemonListen),
'timeout' => config('pterodactyl.guzzle.timeout'), 'timeout' => config('pterodactyl.guzzle.timeout'),
'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'), 'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'),

View file

@ -298,7 +298,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
} }
/** /**
* Get the amount of entries in the database * Get the amount of entries in the database.
* *
* @return int * @return int
*/ */

View file

@ -330,7 +330,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
} }
/** /**
* Get the amount of servers that are suspended * Get the amount of servers that are suspended.
* *
* @return int * @return int
*/ */

View file

@ -91,6 +91,7 @@ class AssetHashService
{ {
return '<link href="' . $this->url($resource) . '" return '<link href="' . $this->url($resource) . '"
rel="stylesheet preload" rel="stylesheet preload"
as="style"
crossorigin="anonymous" crossorigin="anonymous"
integrity="' . $this->integrity($resource) . '" integrity="' . $this->integrity($resource) . '"
referrerpolicy="no-referrer">'; referrerpolicy="no-referrer">';

View file

@ -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\Services\Helpers;
use Ramsey\Uuid\Uuid;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface;
class TemporaryPasswordService
{
const HMAC_ALGO = 'sha256';
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected $connection;
/**
* @var \Illuminate\Contracts\Hashing\Hasher
*/
protected $hasher;
/**
* TemporaryPasswordService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Hashing\Hasher $hasher
*/
public function __construct(ConnectionInterface $connection, Hasher $hasher)
{
$this->connection = $connection;
$this->hasher = $hasher;
}
/**
* Store a password reset token for a specific email address.
*
* @param string $email
* @return string
*/
public function handle($email)
{
$token = hash_hmac(self::HMAC_ALGO, Uuid::uuid4()->toString(), config('app.key'));
$this->connection->table('password_resets')->insert([
'email' => $email,
'token' => $this->hasher->make($token),
]);
return $token;
}
}

View file

@ -78,7 +78,7 @@ class EnvironmentService
} }
// Process variables set in the configuration file. // Process variables set in the configuration file.
foreach ($this->config->get('pterodactyl.environment_mappings', []) as $key => $object) { foreach ($this->config->get('pterodactyl.environment_variables', []) as $key => $object) {
if (is_callable($object)) { if (is_callable($object)) {
$variables[$key] = call_user_func($object, $server); $variables[$key] = call_user_func($object, $server);
} else { } else {

View file

@ -56,6 +56,8 @@ class PermissionCreationService
} }
} }
$this->repository->withoutFreshModel()->insert($insertPermissions); if (! empty($insertPermissions)) {
$this->repository->withoutFreshModel()->insert($insertPermissions);
}
} }
} }

View file

@ -11,17 +11,12 @@ namespace Pterodactyl\Services\Users;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use PragmaRX\Google2FA\Google2FA; use PragmaRX\Google2FA\Google2FA;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class TwoFactorSetupService class TwoFactorSetupService
{ {
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
/** /**
* @var \Illuminate\Contracts\Encryption\Encrypter * @var \Illuminate\Contracts\Encryption\Encrypter
*/ */
@ -40,18 +35,15 @@ class TwoFactorSetupService
/** /**
* TwoFactorSetupService constructor. * TwoFactorSetupService constructor.
* *
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \PragmaRX\Google2FA\Google2FA $google2FA * @param \PragmaRX\Google2FA\Google2FA $google2FA
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/ */
public function __construct( public function __construct(
ConfigRepository $config,
Encrypter $encrypter, Encrypter $encrypter,
Google2FA $google2FA, Google2FA $google2FA,
UserRepositoryInterface $repository UserRepositoryInterface $repository
) { ) {
$this->config = $config;
$this->encrypter = $encrypter; $this->encrypter = $encrypter;
$this->google2FA = $google2FA; $this->google2FA = $google2FA;
$this->repository = $repository; $this->repository = $repository;
@ -62,20 +54,23 @@ class TwoFactorSetupService
* QR code image. * QR code image.
* *
* @param \Pterodactyl\Models\User $user * @param \Pterodactyl\Models\User $user
* @return string * @return \Illuminate\Support\Collection
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function handle(User $user): string public function handle(User $user): Collection
{ {
$secret = $this->google2FA->generateSecretKey($this->config->get('pterodactyl.auth.2fa.bytes')); $secret = $this->google2FA->generateSecretKey(config('pterodactyl.auth.2fa.bytes'));
$image = $this->google2FA->getQRCodeGoogleUrl($this->config->get('app.name'), $user->email, $secret); $image = $this->google2FA->getQRCodeGoogleUrl(config('app.name'), $user->email, $secret);
$this->repository->withoutFreshModel()->update($user->id, [ $this->repository->withoutFreshModel()->update($user->id, [
'totp_secret' => $this->encrypter->encrypt($secret), 'totp_secret' => $this->encrypter->encrypt($secret),
]); ]);
return $image; return new Collection([
'image' => $image,
'secret' => $secret,
]);
} }
} }

View file

@ -5,8 +5,8 @@ namespace Pterodactyl\Services\Users;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Auth\PasswordBroker;
use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Notifications\AccountCreated;
use Pterodactyl\Services\Helpers\TemporaryPasswordService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class UserCreationService class UserCreationService
@ -22,9 +22,9 @@ class UserCreationService
private $hasher; private $hasher;
/** /**
* @var \Pterodactyl\Services\Helpers\TemporaryPasswordService * @var \Illuminate\Contracts\Auth\PasswordBroker
*/ */
private $passwordService; private $passwordBroker;
/** /**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
@ -36,18 +36,18 @@ class UserCreationService
* *
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Hashing\Hasher $hasher * @param \Illuminate\Contracts\Hashing\Hasher $hasher
* @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService * @param \Illuminate\Contracts\Auth\PasswordBroker $passwordBroker
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/ */
public function __construct( public function __construct(
ConnectionInterface $connection, ConnectionInterface $connection,
Hasher $hasher, Hasher $hasher,
TemporaryPasswordService $passwordService, PasswordBroker $passwordBroker,
UserRepositoryInterface $repository UserRepositoryInterface $repository
) { ) {
$this->connection = $connection; $this->connection = $connection;
$this->hasher = $hasher; $this->hasher = $hasher;
$this->passwordService = $passwordService; $this->passwordBroker = $passwordBroker;
$this->repository = $repository; $this->repository = $repository;
} }
@ -68,8 +68,8 @@ class UserCreationService
$this->connection->beginTransaction(); $this->connection->beginTransaction();
if (! isset($data['password']) || empty($data['password'])) { if (! isset($data['password']) || empty($data['password'])) {
$generateResetToken = true;
$data['password'] = $this->hasher->make(str_random(30)); $data['password'] = $this->hasher->make(str_random(30));
$token = $this->passwordService->handle($data['email']);
} }
/** @var \Pterodactyl\Models\User $user */ /** @var \Pterodactyl\Models\User $user */
@ -77,6 +77,10 @@ class UserCreationService
'uuid' => Uuid::uuid4()->toString(), 'uuid' => Uuid::uuid4()->toString(),
]), true, true); ]), true, true);
if (isset($generateResetToken)) {
$token = $this->passwordBroker->createToken($user);
}
$this->connection->commit(); $this->connection->commit();
$user->notify(new AccountCreated($user, $token ?? null)); $user->notify(new AccountCreated($user, $token ?? null));

View file

@ -3,7 +3,7 @@
* Created by PhpStorm. * Created by PhpStorm.
* User: Stan * User: Stan
* Date: 26-5-2018 * Date: 26-5-2018
* Time: 20:56 * Time: 20:56.
*/ */
namespace Pterodactyl\Traits\Controllers; namespace Pterodactyl\Traits\Controllers;
@ -12,13 +12,11 @@ use JavaScript;
trait PlainJavascriptInjection trait PlainJavascriptInjection
{ {
/** /**
* Injects statistics into javascript * Injects statistics into javascript.
*/ */
public function injectJavascript($data) public function injectJavascript($data)
{ {
Javascript::put($data); Javascript::put($data);
} }
}
}

View file

@ -1,36 +0,0 @@
<?php
namespace Pterodactyl\Traits\Helpers;
use Lcobucci\JWT\Signer;
use Illuminate\Support\Str;
trait ProvidesJWTServices
{
/**
* Get the signing key to use when creating JWTs.
*
* @return string
*/
public function getJWTSigningKey(): string
{
$key = config()->get('jwt.key', '');
if (Str::startsWith($key, 'base64:')) {
$key = base64_decode(substr($key, 7));
}
return $key;
}
/**
* Provide the signing algo to use for JWT.
*
* @return \Lcobucci\JWT\Signer
*/
public function getJWTSigner(): Signer
{
$class = config()->get('jwt.signer');
return new $class;
}
}

View file

@ -1,8 +1,6 @@
coverage: coverage:
status: status:
project: project: off
default: patch: off
target: 35
threshold: 1
comment: comment:
layout: "diff" layout: "diff"

View file

@ -26,7 +26,6 @@
"laracasts/utilities": "^3.0", "laracasts/utilities": "^3.0",
"laravel/framework": "5.6.*", "laravel/framework": "5.6.*",
"laravel/tinker": "^1.0", "laravel/tinker": "^1.0",
"lcobucci/jwt": "^3.2",
"lord/laroute": "^2.4", "lord/laroute": "^2.4",
"matriphe/iso-639": "^1.2", "matriphe/iso-639": "^1.2",
"nesbot/carbon": "^1.22", "nesbot/carbon": "^1.22",
@ -90,6 +89,6 @@
"config": { "config": {
"preferred-install": "dist", "preferred-install": "dist",
"sort-packages": true, "sort-packages": true,
"optimize-autoloader": true "optimize-autoloader": false
} }
} }

623
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -34,6 +34,7 @@ return [
'mysql' => [ 'mysql' => [
'driver' => 'mysql', 'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'), 'host' => env('DB_HOST', '127.0.0.1'),
'unix_socket' => env('DB_SOCKET'),
'port' => env('DB_PORT', '3306'), 'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'panel'), 'database' => env('DB_DATABASE', 'panel'),
'username' => env('DB_USERNAME', 'pterodactyl'), 'username' => env('DB_USERNAME', 'pterodactyl'),

View file

@ -1,17 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| JWT Signing Key
|--------------------------------------------------------------------------
|
| This key is used for the verification of JSON Web Tokens in flight and
| should be different than the application encryption key. This key should
| be kept private at all times.
|
*/
'key' => env('APP_JWT_KEY'),
'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
];

View file

@ -28,7 +28,7 @@ return [
| |
*/ */
'lifetime' => env('SESSION_LIFETIME', 10080), 'lifetime' => env('SESSION_LIFETIME', 720),
'expire_on_close' => false, 'expire_on_close' => false,

View file

@ -14,7 +14,7 @@ class AddMaintenanceToNodes extends Migration
public function up() public function up()
{ {
Schema::table('nodes', function (Blueprint $table) { Schema::table('nodes', function (Blueprint $table) {
$table->boolean('maintenance_mode')->after('behind_proxy')->default(false); $table->boolean('maintenance_mode')->after('behind_proxy')->default(false);
}); });
} }
@ -26,7 +26,7 @@ class AddMaintenanceToNodes extends Migration
public function down() public function down()
{ {
Schema::table('nodes', function (Blueprint $table) { Schema::table('nodes', function (Blueprint $table) {
$table->dropColumn('maintenance_mode'); $table->dropColumn('maintenance_mode');
}); });
} }
} }

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-02-27T00:57:04-06:00", "exported_at": "2018-06-25T15:47:07-04:00",
"name": "Forge Minecraft", "name": "Forge Minecraft",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.",
@ -17,7 +17,7 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl\r\n\r\nGET_VERSIONS=$(curl -sl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/ | grep -A1 Latest | grep -o -e '[1]\\.[0-9][0-9]]\\?\\.\\?[0-9]\\?[0-9] - [0-9][0-9]\\.[0-9][0-9]\\.[0-9]\\?[0-9]\\.[0-9][0-9][0-9][0-9]')\r\nLATEST_VERSION=$(echo $GET_VERSIONS | sed 's\/ \/\/g')\r\n\r\ncd \/mnt\/server\r\n\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$LATEST_VERSION\/forge-$LATEST_VERSION-installer.jar -o installer.jar\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$LATEST_VERSION\/forge-$LATEST_VERSION-universal.jar -o server.jar\r\n\r\njava -jar installer.jar --installServer\r\nrm -rf installer.jar", "script": "#!\/bin\/ash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl\r\n\r\nif [ -z \"$MC_VERSION\" ] || [ \"$MC_VERSION\" == \"latest\" ]; then\r\n FORGE_VERSION=$(curl -sl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/ | grep -A1 Latest | grep -o -e '[1]\\.[0-9][0-9]]\\?\\.\\?[0-9]\\?[0-9] - [0-9][0-9]\\.[0-9][0-9]\\.[0-9]\\?[0-9]\\.[0-9][0-9][0-9][0-9]' | sed 's\/ \/\/g')\r\nelse\r\n FORGE_VERSION=$(curl -sl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/index_$MC_VERSION.html | grep -A1 Latest | grep -o -e '[1]\\.[0-9][0-9]]\\?\\.\\?[0-9]\\?[0-9] - [0-9][0-9]\\.[0-9][0-9]\\.[0-9]\\?[0-9]\\.[0-9][0-9][0-9][0-9]' | sed 's\/ \/\/g')\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"\\nDownloading Forge Version $FORGE_VERSION\\n\"\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$FORGE_VERSION\/forge-$FORGE_VERSION-installer.jar -o installer.jar\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$FORGE_VERSION\/forge-$FORGE_VERSION-universal.jar -o $SERVER_JARFILE\r\n\r\necho -e \"\\nInstalling forge server usint the installer jar file.\\n\"\r\njava -jar installer.jar --installServer\r\n\r\necho -e \"\\nDeleting installer jar file and cleaning up.\\n\"\r\nrm -rf installer.jar",
"container": "frolvlad\/alpine-oraclejdk8:cleaned", "container": "frolvlad\/alpine-oraclejdk8:cleaned",
"entrypoint": "ash" "entrypoint": "ash"
} }
@ -31,6 +31,15 @@
"user_viewable": 1, "user_viewable": 1,
"user_editable": 1, "user_editable": 1,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
},
{
"name": "Minecraft version",
"description": "The version of minecraft that you want to run. Example (1.10.2).",
"env_variable": "MC_VERSION",
"default_value": "latest",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|string|max:20"
} }
] ]
} }

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2017-11-03T22:15:07-05:00", "exported_at": "2018-06-19T17:09:18-04:00",
"name": "Vanilla Minecraft", "name": "Vanilla Minecraft",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.", "description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl\r\n\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl -s https:\/\/s3.amazonaws.com\/Minecraft.Download\/versions\/versions.json | grep -o \"[[:digit:]]\\.[0-9]*\\.[0-9]\" | head -n 1`\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n DL_VERSION=$LATEST_VERSION\r\nelse\r\n DL_VERSION=$VANILLA_VERSION\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} https:\/\/s3.amazonaws.com\/Minecraft.Download\/versions\/${DL_VERSION}\/minecraft_server.${DL_VERSION}.jar", "script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl jq\r\n\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq .versions | jq -r '.[] | select(.id == \"'$LATEST_VERSION'\") | .url')\r\nelse\r\n MANIFEST_URL=$(curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq .versions | jq -r '.[] | select(.id == \"'$VANILLA_VERSION'\") | .url')\r\nfi\r\n\r\nDOWNLOAD_URL=`curl $MANIFEST_URL | jq .downloads.server | jq -r '. | .url'`\r\n\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL",
"container": "alpine:3.4", "container": "alpine:3.7",
"entrypoint": "ash" "entrypoint": "ash"
} }
}, },
@ -39,7 +39,7 @@
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": 1,
"user_editable": 1, "user_editable": 1,
"rules": "required|string|between:3,7" "rules": "required|string|between:3,15"
} }
] ]
} }

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-01-21T16:59:47-06:00", "exported_at": "2018-06-19T07:46:06-04:00",
"name": "Counter-Strike: Global Offensive", "name": "Counter-Strike: Global Offensive",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.", "description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.",
@ -11,7 +11,7 @@
"startup": ".\/srcds_run -game csgo -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}", "startup": ".\/srcds_run -game csgo -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}",
"config": { "config": {
"files": "{}", "files": "{}",
"startup": "{\r\n \"done\": \"gameserver Steam ID\",\r\n \"userInteraction\": []\r\n}", "startup": "{\r\n \"done\": \"Connection to Steam servers successful\",\r\n \"userInteraction\": []\r\n}",
"logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}", "logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}",
"stop": "quit" "stop": "quit"
}, },
@ -40,6 +40,15 @@
"user_viewable": 1, "user_viewable": 1,
"user_editable": 1, "user_editable": 1,
"rules": "required|string|alpha_num|size:32" "rules": "required|string|alpha_num|size:32"
},
{
"name": "Source AppID",
"description": "Required for game to update on server restart. Do not modify this.",
"env_variable": "SRCDS_APPID",
"default_value": "740",
"user_viewable": 0,
"user_editable": 0,
"rules": "required|string|max:20"
} }
] ]
} }

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-01-21T16:59:47-06:00", "exported_at": "2018-06-19T07:46:27-04:00",
"name": "Garrys Mod", "name": "Garrys Mod",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.", "description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.",
@ -40,6 +40,15 @@
"user_viewable": 1, "user_viewable": 1,
"user_editable": 1, "user_editable": 1,
"rules": "required|string|alpha_num|size:32" "rules": "required|string|alpha_num|size:32"
},
{
"name": "Source AppID",
"description": "Required for game to update on server restart. Do not modify this.",
"env_variable": "SRCDS_APPID",
"default_value": "4020",
"user_viewable": 0,
"user_editable": 0,
"rules": "required|string|max:20"
} }
] ]
} }

View file

@ -1,6 +1,8 @@
{ {
"name": "pterodactyl-panel", "name": "pterodactyl-panel",
"dependencies": { "dependencies": {
"date-fns": "^1.29.0",
"vee-validate": "^2.1.0-beta.2",
"vue": "^2.5.7", "vue": "^2.5.7",
"vue-axios": "^2.1.1", "vue-axios": "^2.1.1",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
@ -29,9 +31,7 @@
"glob-all": "^3.1.0", "glob-all": "^3.1.0",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"luxon": "^1.2.1",
"postcss": "^6.0.21", "postcss": "^6.0.21",
"postcss-import": "^11.1.0", "postcss-import": "^11.1.0",
"postcss-loader": "^2.1.5", "postcss-loader": "^2.1.5",
@ -61,7 +61,8 @@
"watch": "NODE_ENV=development ./node_modules/.bin/webpack --watch --progress", "watch": "NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",
"build": "NODE_ENV=development ./node_modules/.bin/webpack --progress", "build": "NODE_ENV=development ./node_modules/.bin/webpack --progress",
"build:production": "NODE_ENV=production ./node_modules/.bin/webpack", "build:production": "NODE_ENV=production ./node_modules/.bin/webpack",
"serve": "webpack-serve --hot --config ./webpack.config.js", "serve": "NODE_ENV=development webpack-serve --hot --config ./webpack.config.js --no-clipboard --progress",
"v:serve": "PUBLIC_PATH=http://192.168.50.2:8080 NODE_ENV=development webpack-serve --hot --config ./webpack.config.js --host 192.168.50.2 --no-clipboard" "v:serve": "PUBLIC_PATH=http://pterodactyl.local:8080 NODE_ENV=development webpack-serve --hot --config ./webpack.config.js --host 0.0.0.0 --no-clipboard",
"compile:assets": "php artisan vue-i18n:generate & php artisan ziggy:generate resources/assets/scripts/helpers/ziggy.js"
} }
} }

21
phpunit.dusk.xml Normal file
View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Browser Test Suite">
<directory suffix="Test.php">./tests/Browser</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
</phpunit>

View file

@ -474,6 +474,11 @@ label.control-label > span.field-optional:before {
width: auto; width: auto;
} }
.search01 {
width: 30%;
}
.number-info-box-content { .number-info-box-content {
padding: 15px 10px 0; padding: 15px 10px 0;
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -55,6 +55,10 @@ class ActionsClass {
showLoaderOnConfirm: true, showLoaderOnConfirm: true,
inputValue: inputValue inputValue: inputValue
}, (val) => { }, (val) => {
if (val === false) {
return false;
}
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
headers: { headers: {
@ -100,6 +104,10 @@ class ActionsClass {
showLoaderOnConfirm: true, showLoaderOnConfirm: true,
inputValue: `${currentPath}${currentName}`, inputValue: `${currentPath}${currentName}`,
}, (val) => { }, (val) => {
if (val === false) {
return false;
}
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
headers: { headers: {
@ -233,6 +241,10 @@ class ActionsClass {
showLoaderOnConfirm: true, showLoaderOnConfirm: true,
inputValue: `${currentPath}${currentName}`, inputValue: `${currentPath}${currentName}`,
}, (val) => { }, (val) => {
if (val === false) {
return false;
}
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
headers: { headers: {

View file

@ -2,37 +2,31 @@ import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import vuexI18n from 'vuex-i18n'; import vuexI18n from 'vuex-i18n';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import VeeValidate from 'vee-validate';
Vue.config.productionTip = false;
require('./bootstrap'); require('./bootstrap');
// Helpers // Helpers
import { Ziggy } from './helpers/ziggy'; import { Ziggy } from './helpers/ziggy';
import Locales from './../../../resources/lang/locales'; import Locales from './../../../resources/lang/locales';
import { flash } from './mixins/flash'; import { flash } from './mixins/flash';
import store from './store/index.js';
import { routes } from './routes'; import router from './router';
import createStore from './store';
window.events = new Vue; window.events = new Vue;
window.Ziggy = Ziggy; window.Ziggy = Ziggy;
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'history', routes
});
Vue.use(Vuex); Vue.use(Vuex);
const store = createStore(router); Vue.use(VueRouter);
Vue.use(vuexI18n.plugin, store);
Vue.config.productionTip = false; Vue.use(VeeValidate);
const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default; const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default;
Vue.mixin({ methods: { route } }); Vue.mixin({ methods: { route } });
Vue.mixin(flash); Vue.mixin(flash);
Vue.use(vuexI18n.plugin, store);
Vue.i18n.add('en', Locales.en); Vue.i18n.add('en', Locales.en);
Vue.i18n.set('en'); Vue.i18n.set('en');

View file

@ -1,11 +1,11 @@
<template> <template>
<div> <div>
<form class="bg-white shadow-lg rounded-lg pt-10 px-8 pb-6 mb-4 animate fadein" method="post" <form class="login-box" method="post"
v-on:submit.prevent="submitForm" v-on:submit.prevent="submitForm"
> >
<div class="flex flex-wrap -mx-3 mb-6"> <div class="flex flex-wrap -mx-3 mb-6">
<div class="input-open"> <div class="input-open">
<input class="input" id="grid-email" type="email" aria-labelledby="grid-email-label" required <input class="input open-label" id="grid-email" type="email" aria-labelledby="grid-email-label" required
ref="email" ref="email"
v-bind:class="{ 'has-content': email.length > 0 }" v-bind:class="{ 'has-content': email.length > 0 }"
v-bind:readonly="showSpinner" v-bind:readonly="showSpinner"

View file

@ -1,11 +1,11 @@
<template> <template>
<div> <div>
<form class="bg-white shadow-lg rounded-lg pt-10 px-8 pb-6 mb-4 animate fadein" method="post" <form class="login-box" method="post"
v-on:submit.prevent="submitForm" v-on:submit.prevent="submitForm"
> >
<div class="flex flex-wrap -mx-3 mb-6"> <div class="flex flex-wrap -mx-3 mb-6">
<div class="input-open"> <div class="input-open">
<input class="input" id="grid-username" type="text" name="user" aria-labelledby="grid-username-label" required <input class="input open-label" id="grid-username" type="text" name="user" aria-labelledby="grid-username-label" required
ref="email" ref="email"
:class="{ 'has-content' : user.email.length > 0 }" :class="{ 'has-content' : user.email.length > 0 }"
:readonly="showSpinner" :readonly="showSpinner"
@ -17,7 +17,7 @@
</div> </div>
<div class="flex flex-wrap -mx-3 mb-6"> <div class="flex flex-wrap -mx-3 mb-6">
<div class="input-open"> <div class="input-open">
<input class="input" id="grid-password" type="password" name="password" aria-labelledby="grid-password-label" required <input class="input open-label" id="grid-password" type="password" name="password" aria-labelledby="grid-password-label" required
ref="password" ref="password"
:class="{ 'has-content' : user.password && user.password.length > 0 }" :class="{ 'has-content' : user.password && user.password.length > 0 }"
:readonly="showSpinner" :readonly="showSpinner"
@ -62,7 +62,6 @@
}, },
data: function () { data: function () {
return { return {
errors: [],
showSpinner: false, showSpinner: false,
} }
}, },
@ -91,7 +90,7 @@
this.$props.user.password = ''; this.$props.user.password = '';
this.$data.showSpinner = false; this.$data.showSpinner = false;
this.$refs.password.focus(); this.$refs.password.focus();
this.$store.dispatch('auth/logout'); this.$store.commit('auth/logout');
if (!err.response) { if (!err.response) {
return console.error(err); return console.error(err);

View file

@ -5,7 +5,7 @@
> >
<div class="flex flex-wrap -mx-3 mb-6"> <div class="flex flex-wrap -mx-3 mb-6">
<div class="input-open"> <div class="input-open">
<input class="input" id="grid-email" type="email" aria-labelledby="grid-email" required <input class="input open-label" id="grid-email" type="email" aria-labelledby="grid-email" required
ref="email" ref="email"
:class="{ 'has-content': email.length > 0 }" :class="{ 'has-content': email.length > 0 }"
:readonly="showSpinner" :readonly="showSpinner"
@ -16,7 +16,7 @@
</div> </div>
<div class="flex flex-wrap -mx-3 mb-6"> <div class="flex flex-wrap -mx-3 mb-6">
<div class="input-open"> <div class="input-open">
<input class="input" id="grid-password" type="password" aria-labelledby="grid-password" required <input class="input open-label" id="grid-password" type="password" aria-labelledby="grid-password" required
ref="password" ref="password"
:class="{ 'has-content' : password.length > 0 }" :class="{ 'has-content' : password.length > 0 }"
:readonly="showSpinner" :readonly="showSpinner"
@ -28,7 +28,7 @@
</div> </div>
<div class="flex flex-wrap -mx-3 mb-6"> <div class="flex flex-wrap -mx-3 mb-6">
<div class="input-open"> <div class="input-open">
<input class="input" id="grid-password-confirmation" type="password" aria-labelledby="grid-password-confirmation" required <input class="input open-label" id="grid-password-confirmation" type="password" aria-labelledby="grid-password-confirmation" required
:class="{ 'has-content' : passwordConfirmation.length > 0 }" :class="{ 'has-content' : passwordConfirmation.length > 0 }"
:readonly="showSpinner" :readonly="showSpinner"
v-model="passwordConfirmation" v-model="passwordConfirmation"
@ -97,6 +97,11 @@
throw new Error('An error was encountered while processing this login.'); throw new Error('An error was encountered while processing this login.');
} }
if (response.data.send_to_login) {
self.success('Your password has been reset, please login to continue.');
return self.$router.push({ name: 'login' });
}
return window.location = response.data.redirect_to; return window.location = response.data.redirect_to;
}) })
.catch(function (err) { .catch(function (err) {

View file

@ -1,10 +1,10 @@
<template> <template>
<form class="bg-white shadow-lg rounded-lg pt-10 px-8 pb-6 mb-4 animate fadein" method="post" <form class="login-box" method="post"
v-on:submit.prevent="submitToken" v-on:submit.prevent="submitToken"
> >
<div class="flex flex-wrap -mx-3 mb-6"> <div class="flex flex-wrap -mx-3 mb-6">
<div class="input-open"> <div class="input-open">
<input class="input" id="grid-code" type="number" name="token" aria-labelledby="grid-username" required <input class="input open-label" id="grid-code" type="number" name="token" aria-labelledby="grid-username" required
ref="code" ref="code"
:class="{ 'has-content' : code.length > 0 }" :class="{ 'has-content' : code.length > 0 }"
v-model="code" v-model="code"

View file

@ -0,0 +1,40 @@
<template>
<transition name="modal">
<div class="modal-mask" v-show="show" v-on:click="close">
<div class="modal-container" @click.stop>
<x-icon class="absolute pin-r pin-t m-2 text-grey cursor-pointer" aria-label="Close modal" role="button"
v-on:click="close"
/>
<slot/>
</div>
</div>
</transition>
</template>
<script>
import { XIcon } from 'vue-feather-icons';
export default {
name: 'modal',
components: { XIcon },
props: {
modalName: { type: String, default: 'modal' },
show: { type: Boolean, default: false },
closeOnEsc: { type: Boolean, default: true },
},
mounted: function () {
if (this.$props.closeOnEsc) {
document.addEventListener('keydown', e => {
if (this.show && e.key === 'Escape') {
this.close();
}
})
}
},
methods: {
close: function () {
this.$emit('close', this.$props.modalName);
}
}
};
</script>

View file

@ -9,22 +9,22 @@
<ul> <ul>
<li> <li>
<router-link :to="{ name: 'dashboard' }"> <router-link :to="{ name: 'dashboard' }">
<server-icon aria-label="Server dashboard"/> <server-icon aria-label="Server dashboard" class="h-4"/>
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link :to="{ name: 'account' }"> <router-link :to="{ name: 'account' }">
<user-icon aria-label="Profile management"/> <user-icon aria-label="Profile management" class="h-4"/>
</router-link> </router-link>
</li> </li>
<li> <li>
<a :href="this.route('admin.index')"> <a :href="this.route('admin.index')">
<settings-icon aria-label="Administrative controls"/> <settings-icon aria-label="Administrative controls" class="h-4"/>
</a> </a>
</li> </li>
<li> <li>
<a :href="this.route('auth.logout')"> <a :href="this.route('auth.logout')" v-on:click.prevent="doLogout">
<log-out-icon aria-label="Sign out"/> <log-out-icon aria-label="Sign out" class="h-4"/>
</a> </a>
</li> </li>
</ul> </ul>
@ -37,6 +37,12 @@
export default { export default {
name: 'navigation', name: 'navigation',
components: { LogOutIcon, ServerIcon, SettingsIcon, UserIcon } components: { LogOutIcon, ServerIcon, SettingsIcon, UserIcon },
methods: {
doLogout: function () {
this.$store.commit('auth/logout');
return window.location = this.route('auth.logout');
},
}
}; };
</script> </script>

View file

@ -1,13 +1,51 @@
<template> <template>
<div>
<navigation/>
<div class="container animate fadein mt-2 sm:mt-6">
<modal :show="modalVisible" v-on:close="modalVisible = false">
<TwoFactorAuthentication v-on:close="modalVisible = false"/>
</modal>
<flash container="mt-2 sm:mt-6 mb-2"/>
<div class="flex flex-wrap">
<div class="w-full md:w-1/2">
<div class="sm:m-4 md:ml-0">
<update-email class="mb-4 sm:mb-8"/>
<div class="content-box text-center mb-4 sm:mb-0">
<button class="btn btn-green btn-sm" type="submit" id="grid-open-two-factor-modal"
v-on:click="openModal"
>Configure 2-Factor Authentication</button>
</div>
</div>
</div>
<div class="w-full md:w-1/2">
<change-password class="sm:m-4 md:mr-0"/>
</div>
</div>
</div>
</div>
</template> </template>
<script> <script>
import Navigation from '../core/Navigation';
import Flash from '../Flash';
import UpdateEmail from './account/UpdateEmail';
import ChangePassword from './account/ChangePassword';
import Modal from '../core/Modal';
import TwoFactorAuthentication from './account/TwoFactorAuthentication';
export default { export default {
name: 'account' name: 'account',
components: {TwoFactorAuthentication, Modal, ChangePassword, UpdateEmail, Flash, Navigation},
data: function () {
return {
modalVisible: false,
};
},
methods: {
openModal: function () {
this.$data.modalVisible = true;
window.events.$emit('two_factor:open');
},
}
}; };
</script> </script>
<style scoped>
</style>

View file

@ -11,16 +11,16 @@
ref="search" ref="search"
/> />
</div> </div>
<div v-if="this.isServersLoading" class="my-4 animate fadein"> <div v-if="this.loading" class="my-4 animate fadein">
<div class="text-center h-16"> <div class="text-center h-16">
<span class="spinner spinner-xl"></span> <span class="spinner spinner-xl"></span>
</div> </div>
</div> </div>
<transition-group class="w-full m-auto mt-4 animate fadein sm:flex flex-wrap content-start"> <transition-group class="w-full m-auto mt-4 animate fadein sm:flex flex-wrap content-start" v-else>
<server-box <server-box
v-for="(server, index) in this.serverList" v-for="(server, index) in servers"
:key="index" v-bind:key="index"
:server="server" v-bind:server="server"
/> />
</transition-group> </transition-group>
</div> </div>
@ -28,38 +28,31 @@
</template> </template>
<script> <script>
import { DateTime } from 'luxon';
import Server from '../../models/server'; import Server from '../../models/server';
import _ from 'lodash'; import debounce from 'lodash/debounce';
import differenceInSeconds from 'date-fns/difference_in_seconds';
import Flash from '../Flash'; import Flash from '../Flash';
import ServerBox from './ServerBox'; import ServerBox from './ServerBox';
import Navigation from '../core/Navigation'; import Navigation from '../core/Navigation';
import {mapGetters, mapState} from 'vuex'
export default { export default {
name: 'dashboard', name: 'dashboard',
components: { Navigation, ServerBox, Flash }, components: { Navigation, ServerBox, Flash },
data: function () { data: function () {
return { return {
backgroundedAt: DateTime.local(), backgroundedAt: new Date(),
documentVisible: true,
loading: true,
search: '', search: '',
servers: [],
} }
}, },
computed: {
...mapGetters([
'isServersLoading',
'serverList'
]),
...mapState({
servers: 'servers'
})
},
/** /**
* Start loading the servers before the DOM $.el is created. * Start loading the servers before the DOM $.el is created.
*/ */
created: function () { created: function () {
this.$store.dispatch('loadServers') this.loadServers();
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
this.documentVisible = document.visibilityState === 'visible'; this.documentVisible = document.visibilityState === 'visible';
@ -76,11 +69,50 @@
}, },
methods: { methods: {
/**
* Load the user's servers and render them onto the dashboard.
*
* @param {string} query
*/
loadServers: function (query = '') {
this.loading = true;
window.axios.get(this.route('api.client.index'), {
params: { query },
})
.finally(() => {
this.clearFlashes();
})
.then(response => {
this.servers = [];
response.data.data.forEach(obj => {
const s = new Server(obj.attributes);
this.servers.push(s);
this.getResourceUse(s);
});
if (this.servers.length === 0) {
this.info(this.$t('dashboard.index.no_matches'));
}
})
.catch(err => {
console.error(err);
const response = err.response;
if (response.data && _.isObject(response.data.errors)) {
response.data.errors.forEach(error => {
this.error(error.detail);
});
}
})
.finally(() => {
this.loading = false;
});
},
/** /**
* Handle a search for servers but only call the search function every 500ms * Handle a search for servers but only call the search function every 500ms
* at the fastest. * at the fastest.
*/ */
onChange: _.debounce(function () { onChange: debounce(function () {
this.loadServers(this.$data.search); this.loadServers(this.$data.search);
}, 500), }, 500),
@ -124,14 +156,14 @@
*/ */
_handleDocumentVisibilityChange: function (isVisible) { _handleDocumentVisibilityChange: function (isVisible) {
if (!isVisible) { if (!isVisible) {
this.backgroundedAt = DateTime.local(); this.backgroundedAt = new Date();
return; return;
} }
// If it has been more than 30 seconds since this window was put into the background // If it has been more than 30 seconds since this window was put into the background
// lets go ahead and refresh all of the listed servers so that they have fresh stats. // lets go ahead and refresh all of the listed servers so that they have fresh stats.
const diff = DateTime.local().diff(this.backgroundedAt, 'seconds'); const diff = differenceInSeconds(new Date(), this.backgroundedAt);
this._iterateServerResourceUse(diff.seconds > 30 ? 1 : 5000); this._iterateServerResourceUse(diff > 30 ? 1 : 5000);
}, },
} }
}; };

View file

@ -0,0 +1,91 @@
<template>
<div id="change-password-container" :class>
<form method="post" v-on:submit.prevent="submitForm">
<div class="content-box">
<h2 class="mb-6 text-grey-darkest font-medium">{{ $t('dashboard.account.password.title') }}</h2>
<div class="mt-6">
<label for="grid-password-current" class="input-label">{{ $t('strings.password') }}</label>
<input id="grid-password-current" name="current_password" type="password" class="input" required
ref="current"
v-model="current"
>
</div>
<div class="mt-6">
<label for="grid-password-new" class="input-label">{{ $t('strings.new_password') }}</label>
<input id="grid-password-new" name="password" type="password" class="input" required
:class="{ error: errors.has('password') }"
v-model="newPassword"
v-validate="'min:8'"
>
<p class="input-help error" v-show="errors.has('password')">{{ errors.first('password') }}</p>
<p class="input-help">{{ $t('dashboard.account.password.requirements') }}</p>
</div>
<div class="mt-6">
<label for="grid-password-new-confirm" class="input-label">{{ $t('strings.confirm_password') }}</label>
<input id="grid-password-new-confirm" name="password_confirmation" type="password" class="input" required
:class="{ error: errors.has('password_confirmation') }"
v-model="confirmNew"
v-validate="{is: newPassword}"
data-vv-as="password"
>
<p class="input-help error" v-show="errors.has('password_confirmation')">{{ errors.first('password_confirmation') }}</p>
</div>
<div class="mt-6 text-right">
<button class="btn btn-blue btn-sm text-right" type="submit">{{ $t('strings.save') }}</button>
</div>
</div>
</form>
</div>
</template>
<script>
import isObject from 'lodash/isObject';
export default {
name: 'change-password',
data: function () {
return {
current: '',
newPassword: '',
confirmNew: '',
};
},
methods: {
submitForm: function () {
window.axios.put(this.route('api.client.account.update-password'), {
current_password: this.$data.current,
password: this.$data.newPassword,
password_confirmation: this.$data.confirmNew,
})
.finally(() => {
this.clearFlashes();
this.$validator.pause();
this.$data.current = '';
this.$refs.current.focus();
})
.then(() => {
this.$data.newPassword = '';
this.$data.confirmNew = '';
this.success(this.$t('dashboard.account.password.updated'));
})
.catch(err => {
if (!err.response) {
return console.error(err);
}
const response = err.response;
if (response.data && isObject(response.data.errors)) {
response.data.errors.forEach(error => {
this.error(error.detail);
});
}
})
.finally(() => {
this.$validator.resume();
})
}
}
};
</script>

View file

@ -0,0 +1,191 @@
<template>
<div id="configure-two-factor">
<div class="h-16 text-center" v-show="spinner">
<span class="spinner spinner-xl text-blue"></span>
</div>
<div id="container-disable-two-factor" v-if="response.enabled" v-show="!spinner">
<h2 class="font-medium text-grey-darkest">{{ $t('dashboard.account.two_factor.disable.title') }}</h2>
<div class="mt-6">
<label class="input-label" for="grid-two-factor-token-disable">{{ $t('dashboard.account.two_factor.disable.field') }}</label>
<input id="grid-two-factor-token-disable" type="number" class="input"
name="token"
v-model="token"
ref="token"
v-validate="'length:6'"
:class="{ error: errors.has('token') }"
>
<p class="input-help error" v-show="errors.has('token')">{{ errors.first('token') }}</p>
</div>
<div class="mt-6 w-full text-right">
<button class="btn btn-sm btn-secondary mr-4" v-on:click="$emit('close')">
Cancel
</button>
<button class="btn btn-sm btn-red" type="submit"
:disabled="submitDisabled"
v-on:click.prevent="disableTwoFactor"
>{{ $t('strings.disable') }}</button>
</div>
</div>
<div id="container-enable-two-factor" v-else v-show="!spinner">
<h2 class="font-medium text-grey-darkest">{{ $t('dashboard.account.two_factor.setup.title') }}</h2>
<div class="flex mt-6">
<div class="flex-none w-full sm:w-1/2 text-center">
<div class="h-48">
<img :src="response.qr_image" id="grid-qr-code" alt="Two-factor qr image" class="h-48">
</div>
<div>
<p class="text-xs text-grey-darker mb-2">{{ $t('dashboard.account.two_factor.setup.help') }}</p>
<p class="text-xs"><code>{{response.secret}}</code></p>
</div>
</div>
<div class="flex-none w-full sm:w-1/2">
<div>
<label class="input-label" for="grid-two-factor-token">{{ $t('dashboard.account.two_factor.setup.field') }}</label>
<input id="grid-two-factor-token" type="number" class="input"
name="token"
v-model="token"
ref="token"
v-validate="'length:6'"
:class="{ error: errors.has('token') }"
>
<p class="input-help error" v-show="errors.has('token')">{{ errors.first('token') }}</p>
</div>
<div class="mt-6">
<button class="btn btn-blue btn-jumbo" type="submit"
:disabled="submitDisabled"
v-on:click.prevent="enableTwoFactor"
>{{ $t('strings.enable') }}</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from 'vue';
import isObject from 'lodash/isObject';
export default {
name: 'TwoFactorAuthentication',
data: function () {
return {
spinner: true,
token: '',
submitDisabled: true,
response: {
enabled: false,
qr_image: '',
secret: '',
},
};
},
/**
* Before the component is mounted setup the event listener. This event is fired when a user
* presses the 'Configure 2-Factor' button on their account page. Once this happens we fire off
* a HTTP request to get their information.
*/
mounted: function () {
window.events.$on('two_factor:open', () => {
this.prepareModalContent();
});
},
watch: {
token: function (value) {
this.$data.submitDisabled = value.length !== 6;
},
},
methods: {
/**
* Determine the correct content to show in the modal.
*/
prepareModalContent: function () {
// Reset the data object when the modal is opened again.
Object.assign(this.$data, this.$options.data());
window.axios.get(this.route('account.two_factor'))
.finally(() => {
this.clearFlashes();
})
.then(response => {
this.$data.response = response.data;
this.$data.spinner = false;
Vue.nextTick().then(() => {
this.$refs.token.focus();
})
})
.catch(error => {
if (!error.response) {
this.error(error.message);
}
const response = error.response;
if (response.data && isObject(response.data.errors)) {
response.data.errors.forEach(e => {
this.error(e.detail);
});
}
this.$emit('close');
});
},
/**
* Enable two-factor authentication on the account by validating the token provided by the user.
* Close the modal once the request completes so that the success or error message can be shown
* to the user.
*/
enableTwoFactor: function () {
return this._callInternalApi('account.two_factor.enable', 'enabled');
},
/**
* Disables two-factor authentication for the client account and closes the modal.
*/
disableTwoFactor: function () {
return this._callInternalApi('account.two_factor.disable', 'disabled');
},
/**
* Call the Panel API endpoint and handle errors.
*
* @param {String} route
* @param {String} langKey
* @private
*/
_callInternalApi: function (route, langKey) {
window.axios.post(this.route(route), {
token: this.$data.token,
})
.finally(() => {
this.clearFlashes();
})
.then(response => {
if (response.data.success) {
this.success(this.$t(`dashboard.account.two_factor.${langKey}`));
} else {
this.error(this.$t('dashboard.account.two_factor.invalid'));
}
})
.catch(error => {
if (!error.response) {
this.error(error.message);
}
const response = error.response;
if (response.data && isObject(response.data.errors)) {
response.data.errors.forEach(e => {
this.error(e.detail);
});
}
})
.finally(() => {
this.$emit('close');
})
}
},
};
</script>

View file

@ -0,0 +1,81 @@
<template>
<div id="update-email-container" :class>
<form method="post" v-on:submit.prevent="submitForm">
<div class="content-box">
<h2 class="mb-6 text-grey-darkest font-medium">{{ $t('dashboard.account.email.title') }}</h2>
<div>
<label for="grid-email" class="input-label">{{ $t('strings.email_address') }}</label>
<input id="grid-email" name="email" type="email" class="input" required
:class="{ error: errors.has('email') }"
v-validate
v-model="email"
>
<p class="input-help error" v-show="errors.has('email')">{{ errors.first('email') }}</p>
</div>
<div class="mt-6">
<label for="grid-password" class="input-label">{{ $t('strings.password') }}</label>
<input id="grid-password" name="password" type="password" class="input" required
v-model="password"
>
</div>
<div class="mt-6 text-right">
<button class="btn btn-blue btn-sm text-right" type="submit">{{ $t('strings.save') }}</button>
</div>
</div>
</form>
</div>
</template>
<script>
import { isObject, get } from 'lodash';
import { mapState, mapActions } from 'vuex';
export default {
name: 'update-email',
data: function () {
return {
email: get(this.$store.state, 'auth.user.email', ''),
password: '',
};
},
computed: {
...mapState({
user: state => state.auth.user,
})
},
methods: {
/**
* Update a user's email address on the Panel.
*/
submitForm: function () {
this.clearFlashes();
this.updateEmail({
email: this.$data.email,
password: this.$data.password
})
.finally(() => {
this.$data.password = '';
})
.then(() => {
this.success(this.$t('dashboard.account.email.updated'));
})
.catch(error => {
if (!error.response) {
this.error(error.message);
}
const response = error.response;
if (response.data && isObject(response.data.errors)) {
response.data.errors.forEach(e => {
this.error(e.detail);
});
}
});
},
...mapActions('auth', [
'updateEmail',
])
}
};
</script>

View file

@ -1,5 +1,3 @@
import User from './../models/user';
/** /**
* We'll load the axios HTTP library which allows us to easily issue requests * We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the * to our Laravel back-end. This library automatically handles sending the
@ -9,7 +7,6 @@ import User from './../models/user';
let axios = require('axios'); let axios = require('axios');
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.headers.common['Accept'] = 'application/json'; axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.common['Authorization'] = `Bearer ${User.getToken()}`;
if (typeof phpdebugbar !== 'undefined') { if (typeof phpdebugbar !== 'undefined') {
axios.interceptors.response.use(function (response) { axios.interceptors.response.use(function (response) {

File diff suppressed because one or more lines are too long

View file

@ -1,39 +1,4 @@
import isString from 'lodash/isString';
import jwtDecode from 'jwt-decode';
export default class User { export default class User {
/**
* Get a new user model from the JWT.
*
* @return {User | null}
*/
static fromToken(token) {
if (!isString(token)) {
token = localStorage.getItem('token');
}
if (!isString(token) || token.length < 1) {
return null;
}
const data = jwtDecode(token);
if (data.user) {
return new User(data.user);
}
return null;
}
/**
* Return the JWT for the authenticated user.
*
* @returns {string | null}
*/
static getToken()
{
return localStorage.getItem('token');
}
/** /**
* Create a new user model. * Create a new user model.
* *
@ -45,14 +10,14 @@ export default class User {
* @param {String} language * @param {String} language
*/ */
constructor({ constructor({
admin, root_admin,
username, username,
email, email,
first_name, first_name,
last_name, last_name,
language, language,
}) { }) {
this.admin = admin; this.admin = root_admin;
this.username = username; this.username = username;
this.email = email; this.email = email;
this.name = `${first_name} ${last_name}`; this.name = `${first_name} ${last_name}`;

View file

@ -0,0 +1,66 @@
import VueRouter from 'vue-router';
import store from './store/index';
import compareDate from 'date-fns/compare_asc'
import addHours from 'date-fns/add_hours'
import dateParse from 'date-fns/parse'
const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default;
// Base Vuejs Templates
import Login from './components/auth/Login';
import Dashboard from './components/dashboard/Dashboard';
import Account from './components/dashboard/Account';
import ResetPassword from './components/auth/ResetPassword';
import User from './models/user';
const routes = [
{ name: 'login', path: '/auth/login', component: Login },
{ name: 'forgot-password', path: '/auth/password', component: Login },
{ name: 'checkpoint', path: '/auth/checkpoint', component: Login },
{
name: 'reset-password',
path: '/auth/password/reset/:token',
component: ResetPassword,
props: function (route) {
return { token: route.params.token, email: route.query.email || '' };
}
},
{ name : 'dashboard', path: '/', component: Dashboard },
{ name : 'account', path: '/account', component: Account },
{ name : 'account.api', path: '/account/api', component: Account },
{ name : 'account.security', path: '/account/security', component: Account },
{
name: 'server',
path: '/server/:id',
// component: Server,
// children: [
// { path: 'files', component: ServerFileManager }
// ],
}
];
const router = new VueRouter({
mode: 'history', routes
});
// Redirect the user to the login page if they try to access a protected route and
// have no JWT or the JWT is expired and wouldn't be accepted by the Panel.
router.beforeEach((to, from, next) => {
if (to.path === route('auth.logout')) {
return window.location = route('auth.logout');
}
const user = store.getters['auth/getUser'];
// Check that if we're accessing a non-auth route that a user exists on the page.
if (!to.path.startsWith('/auth') && !(user instanceof User)) {
store.commit('auth/logout');
return window.location = route('auth.logout');
}
// Continue on through the pipeline.
return next();
});
export default router;

View file

@ -1,20 +1,24 @@
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { sync } from 'vuex-router-sync';
import { serverModule } from "./modules/server"; import { serverModule } from "./modules/server";
import { userModule } from './modules/user'; import { userModule } from './modules/user';
import { authModule } from "./modules/auth"; import { authModule } from "./modules/auth";
const createStore = (router) => { Vue.use(Vuex);
const store = new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
modules: {
userModule,
serverModule,
authModule,
},
});
sync(store, router);
return store;
};
export default createStore; const store = new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
modules: { userModule, serverModule, authModule },
});
if (module.hot) {
module.hot.accept(['./modules/auth'], () => {
const newAuthModule = require('./modules/auth').default;
store.hotUpdate({
modules: { newAuthModule },
});
});
}
export default store;

View file

@ -1,10 +1,11 @@
import User from './../../models/user'; import User from './../../models/user';
const route = require('./../../../../../vendor/tightenco/ziggy/src/js/route').default; const route = require('./../../../../../vendor/tightenco/ziggy/src/js/route').default;
export const authModule = { export const authModule = {
namespaced: true, namespaced: true,
state: { state: {
user: User.fromToken(), user: typeof window.PterodactylUser === 'object' ? new User(window.PterodactylUser) : null,
}, },
getters: { getters: {
/** /**
@ -13,12 +14,20 @@ export const authModule = {
* @param state * @param state
* @returns {User|null} * @returns {User|null}
*/ */
currentUser: function (state) { getUser: function (state) {
return state.user; return state.user;
} },
}, },
setters: {}, setters: {},
actions: { actions: {
/**
* Log a user into the Panel.
*
* @param commit
* @param {String} user
* @param {String} password
* @returns {Promise<any>}
*/
login: ({commit}, {user, password}) => { login: ({commit}, {user, password}) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
window.axios.post(route('auth.login'), {user, password}) window.axios.post(route('auth.login'), {user, password})
@ -32,7 +41,7 @@ export const authModule = {
} }
if (response.data.complete) { if (response.data.complete) {
commit('login', {jwt: response.data.jwt}); commit('login', response.data.user);
return resolve({ return resolve({
complete: true, complete: true,
intended: response.data.intended, intended: response.data.intended,
@ -47,24 +56,40 @@ export const authModule = {
.catch(reject); .catch(reject);
}); });
}, },
logout: function ({commit}) {
/**
* Update a user's email address on the Panel and store the updated result in Vuex.
*
* @param commit
* @param {String} email
* @param {String} password
* @return {Promise<any>}
*/
updateEmail: function ({commit}, {email, password}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
window.axios.get(route('auth.logout')) window.axios.put(route('api.client.account.update-email'), {email, password})
.then(() => { .then(response => {
commit('logout'); // If there is a 302 redirect or some other odd behavior (basically, response that isnt
// in JSON format) throw an error and don't try to continue with the login.
if (!(response.data instanceof Object) && response.status !== 201) {
return reject(new Error('An error was encountered while processing this request.'));
}
commit('setEmail', email);
return resolve(); return resolve();
}) })
.catch(reject); .catch(reject);
}) });
}, },
}, },
mutations: { mutations: {
login: function (state, {jwt}) { setEmail: function (state, email) {
localStorage.setItem('token', jwt); state.user.email = email;
state.user = User.fromToken(jwt); },
login: function (state, data) {
state.user = new User(data);
}, },
logout: function (state) { logout: function (state) {
localStorage.removeItem('token');
state.user = null; state.user = null;
}, },
}, },

View file

@ -34,3 +34,15 @@
@apply .bg-red-dark; @apply .bg-red-dark;
} }
} }
/*
* transition="modal"
*/
.modal-enter, .modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
animation: opacity 250ms linear;
}

View file

@ -1,3 +1,9 @@
.login-box { .login-box {
@apply .bg-white .shadow-lg .rounded-lg .pt-10 .px-8 .pb-6 .mb-4; @apply .bg-white .shadow-lg .rounded-lg .pt-10 .px-8 .pb-6 .mb-4;
@screen xsx {
@apply .rounded-none;
margin-top: 25%;
box-shadow: 0 15px 30px 0 rgba(0, 0, 0, .2), 0 -15px 30px 0 rgba(0, 0, 0, .2);
}
} }

View file

@ -12,18 +12,41 @@
} }
} }
&.btn-green {
@apply .bg-green .border-green-dark .border .text-white;
&:hover:enabled {
@apply .bg-green-dark .border-green-darker;
}
}
&.btn-red { &.btn-red {
@apply .bg-red .border-red-dark .border .text-white; @apply .bg-red .border-red-dark .border .text-white;
&:hover:enabled { &:hover:enabled {
@apply .bg-red-dark .border-red-darker; @apply .bg-red-dark .border-red-darker;
} }
} }
/* Button Sizes */
&.btn-secondary {
@apply .border .border-grey-light .text-grey-dark;
&:hover:enabled {
@apply .border-grey .text-grey-darker;
}
}
/**
* Button Sizes
*/
&.btn-jumbo { &.btn-jumbo {
@apply .p-4 .w-full .uppercase .tracking-wide .text-sm; @apply .p-4 .w-full .uppercase .tracking-wide .text-sm;
} }
&.btn-sm {
@apply .px-6 .py-3 .uppercase .tracking-wide .text-sm;
}
&:disabled, &.disabled { &:disabled, &.disabled {
opacity: 0.55; opacity: 0.55;
cursor: default; cursor: default;

View file

@ -1,3 +1,17 @@
textarea, select, input, button {
outline: none;
}
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield !important;
}
/** /**
* Styles for the login form open input boxes. Label floats up above it when content * Styles for the login form open input boxes. Label floats up above it when content
* is input and then sinks back down into the field if left empty. * is input and then sinks back down into the field if left empty.
@ -30,3 +44,35 @@
top: 14px; top: 14px;
transition: transform 200ms ease-out; transition: transform 200ms ease-out;
} }
/**
* Styling for other forms throughout the Panel.
*/
.input:not(.open-label) {
@apply .appearance-none .p-3 .rounded .border .text-grey-darker .w-full;
transition: all 100ms linear;
&:focus {
@apply .border-blue-light;
}
&:required, &:invalid {
box-shadow: none;
}
&.error {
@apply .text-red-dark .border-red;
}
}
.input-label {
@apply .block .uppercase .tracking-wide .text-grey-darkest .text-xs .font-bold .mb-2;
}
.input-help {
@apply .text-xs .text-grey .pt-2;
&.error {
@apply .text-red-dark;
}
}

View file

@ -42,6 +42,13 @@ code {
} }
} }
/**
* Styling for elements that contain the core page content.
*/
.content-box {
@apply .bg-white .p-6 .border .border-grey-light .rounded;
}
/** /**
* Flex boxes for server listing on user dashboard. * Flex boxes for server listing on user dashboard.
*/ */

View file

@ -0,0 +1,20 @@
.modal-mask {
@apply .fixed .pin .z-50 .overflow-auto .flex;
background: rgba(0, 0, 0, 0.7);
transition: opacity 250ms ease;
& > .modal-container {
@apply .relative .p-8 .bg-white .w-full .max-w-md .m-auto .flex-col .flex;
transition: all 250ms ease;
margin-top: 15%;
/**
* On tiny phone screens make sure there is a margin on the sides and also
* center the modal rather than putting it towards the top of the screen.
*/
@screen smx {
margin-top: auto;
width: 90%;
}
}
}

View file

@ -27,10 +27,6 @@
&:hover { &:hover {
@apply .bg-blue-dark; @apply .bg-blue-dark;
} }
& .feather {
@apply .h-4;
}
} }
} }
} }

View file

@ -31,11 +31,11 @@
/** /**
* Spinner Colors * Spinner Colors
*/ */
&.blue:after { &.blue:after, &.text-blue:after {
@apply .border-blue; @apply .border-blue;
} }
&.white:after { &.white:after, &.text-white:after {
@apply .border-white; @apply .border-white;
} }

View file

@ -13,6 +13,7 @@
@import "components/containers.css"; @import "components/containers.css";
@import "components/forms.css"; @import "components/forms.css";
@import "components/miscellaneous.css"; @import "components/miscellaneous.css";
@import "components/modal.css";
@import "components/navigation.css"; @import "components/navigation.css";
@import "components/notifications.css"; @import "components/notifications.css";
@import "components/spinners.css"; @import "components/spinners.css";

View file

@ -48,7 +48,7 @@ return [
'select_none' => 'Alles abwählen', 'select_none' => 'Alles abwählen',
'alias' => 'Alias', 'alias' => 'Alias',
'primary' => 'Primär', 'primary' => 'Primär',
'make_primary' => 'Primät machen', 'make_primary' => 'Primär machen',
'none' => 'Nichts', 'none' => 'Nichts',
'cancel' => 'Abbrechen', 'cancel' => 'Abbrechen',
'created_at' => 'Erstellt am', 'created_at' => 'Erstellt am',

View file

@ -54,35 +54,4 @@ return [
], ],
], ],
], ],
'account' => [
'details_updated' => 'Your account details have been successfully updated.',
'invalid_password' => 'The password provided for your account was not valid.',
'header' => 'Your Account',
'header_sub' => 'Manage your account details.',
'update_pass' => 'Update Password',
'update_email' => 'Update Email Address',
'current_password' => 'Current Password',
'new_password' => 'New Password',
'new_password_again' => 'Repeat New Password',
'new_email' => 'New Email Address',
'first_name' => 'First Name',
'last_name' => 'Last Name',
'update_identity' => 'Update Identity',
'username_help' => 'Your username must be unique to your account, and may only contain the following characters: :requirements.',
],
'security' => [
'session_mgmt_disabled' => 'Your host has not enabled the ability to manage account sessions via this interface.',
'header' => 'Account Security',
'header_sub' => 'Control active sessions and 2-Factor Authentication.',
'sessions' => 'Active Sessions',
'2fa_header' => '2-Factor Authentication',
'2fa_token_help' => 'Enter the 2FA Token generated by your app (Google Authenticator, Authy, etc.).',
'disable_2fa' => 'Disable 2-Factor Authentication',
'2fa_enabled' => '2-Factor Authentication is enabled on this account and will be required in order to login to the panel. If you would like to disable 2FA, simply enter a valid token below and submit the form.',
'2fa_disabled' => '2-Factor Authentication is disabled on your account! You should enable 2FA in order to add an extra level of protection on your account.',
'enable_2fa' => 'Enable 2-Factor Authentication',
'2fa_qr' => 'Configure 2FA on Your Device',
'2fa_checkpoint_help' => 'Use the 2FA application on your phone to take a picture of the QR code to the left, or manually enter the code under it. Once you have done so, generate a token and enter it below.',
'2fa_disable_error' => 'The 2FA token provided was not valid. Protection has not been disabled for this account.',
],
]; ];

View file

@ -0,0 +1,28 @@
<?php
return [
'email' => [
'title' => 'Update your email',
'updated' => 'Your email address has been updated.',
],
'password' => [
'title' => 'Change your password',
'requirements' => 'Your new password should be at least 8 characters in length.',
'updated' => 'Your password has been updated.',
],
'two_factor' => [
'button' => 'Configure 2-Factor Authentication',
'disabled' => 'Two-factor authentication has been disabled on your account. You will no longer be prompted to provide a token when logging in.',
'enabled' => 'Two-factor authentication has been enabled on your account! From now on, when logging in, you will be required to provide the code generated by your device.',
'invalid' => 'The token provided was invalid.',
'setup' => [
'title' => 'Setup two-factor authentication',
'help' => 'Can\'t scan the code? Enter the code below into your application:',
'field' => 'Enter token',
],
'disable' => [
'title' => 'Disable two-factor authentication',
'field' => 'Enter token',
],
],
];

View file

@ -2,9 +2,11 @@
return [ return [
'email' => 'Email', 'email' => 'Email',
'email_address' => 'Email address',
'user_identifier' => 'Username or Email', 'user_identifier' => 'Username or Email',
'password' => 'Password', 'password' => 'Password',
'confirm_password' => 'Confirm Password', 'new_password' => 'New password',
'confirm_password' => 'Confirm new password',
'login' => 'Login', 'login' => 'Login',
'home' => 'Home', 'home' => 'Home',
'servers' => 'Servers', 'servers' => 'Servers',
@ -85,7 +87,8 @@ return [
'sat' => 'Saturday', 'sat' => 'Saturday',
], ],
'last_used' => 'Last Used', 'last_used' => 'Last Used',
'enable' => 'Enable',
// Copyright Line 'disable' => 'Disable',
'save' => 'Save',
'copyright' => '&copy; 2015 - :year Pterodactyl Software', 'copyright' => '&copy; 2015 - :year Pterodactyl Software',
]; ];

View file

@ -101,5 +101,6 @@ return [
// Internal validation logic for Pterodactyl // Internal validation logic for Pterodactyl
'internal' => [ 'internal' => [
'variable_value' => ':env variable', 'variable_value' => ':env variable',
'invalid_password' => 'The password provided was invalid for this account.',
], ],
]; ];

View file

@ -62,7 +62,7 @@
<h3 class="box-title">Force Delete Server</h3> <h3 class="box-title">Force Delete Server</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
<p>This action will attempt to delete the server from both the panel and daemon. The the daemon does not respond, or reports an error the deletion will continue.</p> <p>This action will attempt to delete the server from both the panel and daemon. If the daemon does not respond, or reports an error the deletion will continue.</p>
<p class="text-danger small">Deleting a server is an irreversible action. <strong>All server data</strong> (including files and users) will be removed from the system. This method may leave dangling files on your daemon if it reports an error.</p> <p class="text-danger small">Deleting a server is an irreversible action. <strong>All server data</strong> (including files and users) will be removed from the system. This method may leave dangling files on your daemon if it reports an error.</p>
</div> </div>
<div class="box-footer"> <div class="box-footer">

View file

@ -23,10 +23,10 @@
<div class="box"> <div class="box">
<div class="box-header"> <div class="box-header">
<h3 class="box-title">@lang('base.index.list')</h3> <h3 class="box-title">@lang('base.index.list')</h3>
<div class="box-tools"> <div class="box-tools search01">
<form action="{{ route('index') }}" method="GET"> <form action="{{ route('index') }}" method="GET">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="text" name="query" class="form-control pull-right" style="width:30%;" value="{{ request()->input('query') }}" placeholder="@lang('strings.search')"> <input type="text" name="query" class="form-control pull-right" value="{{ request()->input('query') }}" placeholder="@lang('strings.search')">
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="btn btn-default"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-default"><i class="fa fa-search"></i></button>
</div> </div>

View file

@ -146,7 +146,7 @@
</a> </a>
</li> </li>
@endcan @endcan
@can('list-tasks', $server) @can('list-schedules', $server)
<li <li
@if(starts_with(Route::currentRouteName(), 'server.schedules')) @if(starts_with(Route::currentRouteName(), 'server.schedules'))
class="active" class="active"

View file

@ -44,7 +44,7 @@
{!! Theme::js('vendor/lodash/lodash.js') !!} {!! Theme::js('vendor/lodash/lodash.js') !!}
{!! Theme::js('vendor/siofu/client.min.js') !!} {!! Theme::js('vendor/siofu/client.min.js') !!}
@if(App::environment('production')) @if(App::environment('production'))
{!! Theme::js('js/frontend/files/filemanager.min.js') !!} {!! Theme::js('js/frontend/files/filemanager.min.js?updated-cancel-buttons') !!}
@else @else
{!! Theme::js('js/frontend/files/src/index.js') !!} {!! Theme::js('js/frontend/files/src/index.js') !!}
{!! Theme::js('js/frontend/files/src/contextmenu.js') !!} {!! Theme::js('js/frontend/files/src/contextmenu.js') !!}

View file

@ -4,7 +4,7 @@
@section('container') @section('container')
<div class="w-full max-w-xs sm:max-w-sm m-auto mt-8"> <div class="w-full max-w-xs sm:max-w-sm m-auto mt-8">
<div class="text-center"> <div class="text-center hidden sm:block">
<img src="/assets/img/pterodactyl-flat.svg" class="max-w-xxs"> <img src="/assets/img/pterodactyl-flat.svg" class="max-w-xxs">
</div> </div>
<router-view></router-view> <router-view></router-view>

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