diff --git a/.php_cs b/.php_cs
index aca934c8..fe3dfb56 100644
--- a/.php_cs
+++ b/.php_cs
@@ -25,6 +25,10 @@ return PhpCsFixer\Config::create()
'declare_equal_normalize' => ['space' => 'single'],
'heredoc_to_nowdoc' => true,
'linebreak_after_opening_tag' => true,
+ 'method_argument_space' => [
+ 'ensure_fully_multiline' => false,
+ 'keep_multiple_spaces_after_comma' => false,
+ ],
'new_with_braces' => false,
'no_alias_functions' => true,
'no_multiline_whitespace_before_semicolons' => true,
diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php
index 42bfb975..70373654 100644
--- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php
+++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php
@@ -88,4 +88,11 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface
* @return \Psr\Http\Message\ResponseInterface
*/
public function delete();
+
+ /**
+ * Return detials on a specific server.
+ *
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function details();
}
diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php
index 15c3a416..44450ea4 100644
--- a/app/Contracts/Repository/RepositoryInterface.php
+++ b/app/Contracts/Repository/RepositoryInterface.php
@@ -74,11 +74,12 @@ interface RepositoryInterface
*
* @param array $fields
* @param bool $validate
+ * @param bool $force
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
- public function create(array $fields, $validate = true);
+ public function create(array $fields, $validate = true, $force = false);
/**
* Delete a given record from the database.
diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php
index d71a349a..9413b160 100644
--- a/app/Contracts/Repository/ServerRepositoryInterface.php
+++ b/app/Contracts/Repository/ServerRepositoryInterface.php
@@ -87,4 +87,33 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getDaemonServiceData($id);
+
+ /**
+ * Return an array of server IDs that a given user can access based on owner and subuser permissions.
+ *
+ * @param int $user
+ * @return array
+ */
+ public function getUserAccessServers($user);
+
+ /**
+ * Return a paginated list of servers that a user can access at a given level.
+ *
+ * @param int $user
+ * @param string $level
+ * @param bool $admin
+ * @param array $relations
+ * @return \Illuminate\Pagination\LengthAwarePaginator
+ */
+ public function filterUserAccessServers($user, $admin = false, $level = 'all', array $relations = []);
+
+ /**
+ * Return a server by UUID.
+ *
+ * @param string $uuid
+ * @return \Illuminate\Database\Eloquent\Collection
+ *
+ * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
+ */
+ public function getByUuid($uuid);
}
diff --git a/app/Http/Controllers/Base/LanguageController.php b/app/Contracts/Repository/SessionRepositoryInterface.php
similarity index 51%
rename from app/Http/Controllers/Base/LanguageController.php
rename to app/Contracts/Repository/SessionRepositoryInterface.php
index 0addd218..a4f0f3e9 100644
--- a/app/Http/Controllers/Base/LanguageController.php
+++ b/app/Contracts/Repository/SessionRepositoryInterface.php
@@ -1,5 +1,5 @@
.
*
@@ -22,50 +22,24 @@
* SOFTWARE.
*/
-namespace Pterodactyl\Http\Controllers\Base;
+namespace Pterodactyl\Contracts\Repository;
-use Auth;
-use Session;
-use Illuminate\Http\Request;
-use Pterodactyl\Models\User;
-use Pterodactyl\Http\Controllers\Controller;
-
-class LanguageController extends Controller
+interface SessionRepositoryInterface extends RepositoryInterface
{
/**
- * A list of supported languages on the panel.
+ * Delete a session for a given user.
*
- * @var array
+ * @param int $user
+ * @param int $session
+ * @return null|int
*/
- protected $languages = [
- 'de' => 'German',
- 'en' => 'English',
- 'et' => 'Estonian',
- 'nb' => 'Norwegian',
- 'nl' => 'Dutch',
- 'pt' => 'Portuguese',
- 'ro' => 'Romanian',
- 'ru' => 'Russian',
- ];
+ public function deleteUserSession($user, $session);
/**
- * Sets the language for a user.
+ * Return all of the active sessions for a user.
*
- * @param \Illuminate\Http\Request $request
- * @param string $language
- * @return \Illuminate\Http\RedirectResponse
+ * @param int $user
+ * @return \Illuminate\Support\Collection
*/
- public function setLanguage(Request $request, $language)
- {
- if (array_key_exists($language, $this->languages)) {
- if (Auth::check()) {
- $user = User::findOrFail(Auth::user()->id);
- $user->language = $language;
- $user->save();
- }
- Session::put('applocale', $language);
- }
-
- return redirect()->back();
- }
+ public function getUserSessions($user);
}
diff --git a/app/Exceptions/Http/Base/InvalidPasswordProvidedException.php b/app/Exceptions/Http/Base/InvalidPasswordProvidedException.php
new file mode 100644
index 00000000..3b4fce10
--- /dev/null
+++ b/app/Exceptions/Http/Base/InvalidPasswordProvidedException.php
@@ -0,0 +1,31 @@
+.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+namespace Pterodactyl\Exceptions\Http\Base;
+
+use Pterodactyl\Exceptions\DisplayException;
+
+class InvalidPasswordProvidedException extends DisplayException
+{
+}
diff --git a/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php b/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php
new file mode 100644
index 00000000..1e7c6483
--- /dev/null
+++ b/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php
@@ -0,0 +1,29 @@
+.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+namespace Pterodactyl\Exceptions\Service\User;
+
+class TwoFactorAuthenticationTokenInvalid extends \Exception
+{
+}
diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php
index 7d7e3952..72a4e7b6 100644
--- a/app/Http/Controllers/Base/APIController.php
+++ b/app/Http/Controllers/Base/APIController.php
@@ -27,12 +27,11 @@ namespace Pterodactyl\Http\Controllers\Base;
use Illuminate\Http\Request;
use Prologue\Alerts\AlertsMessageBag;
+use Pterodactyl\Http\Requests\Base\ApiKeyFormRequest;
use Pterodactyl\Models\APIPermission;
-use Pterodactyl\Services\ApiKeyService;
use Pterodactyl\Http\Controllers\Controller;
-use Pterodactyl\Http\Requests\ApiKeyRequest;
-use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
+use Pterodactyl\Services\Api\KeyCreationService;
class APIController extends Controller
{
@@ -41,31 +40,31 @@ class APIController extends Controller
*/
protected $alert;
+ /**
+ * @var \Pterodactyl\Services\Api\KeyCreationService
+ */
+ protected $keyService;
+
/**
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
*/
protected $repository;
- /**
- * @var \Pterodactyl\Services\ApiKeyService
- */
- protected $service;
-
/**
* APIController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
- * @param \Pterodactyl\Services\ApiKeyService $service
+ * @param \Pterodactyl\Services\Api\KeyCreationService $keyService
*/
public function __construct(
AlertsMessageBag $alert,
ApiKeyRepositoryInterface $repository,
- ApiKeyService $service
+ KeyCreationService $keyService
) {
$this->alert = $alert;
+ $this->keyService = $keyService;
$this->repository = $repository;
- $this->service = $service;
}
/**
@@ -73,6 +72,8 @@ class APIController extends Controller
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
+ *
+ * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function index(Request $request)
{
@@ -84,14 +85,15 @@ class APIController extends Controller
/**
* Display API key creation page.
*
+ * @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
- public function create()
+ public function create(Request $request)
{
return view('base.api.new', [
'permissions' => [
'user' => collect(APIPermission::CONST_PERMISSIONS)->pull('_user'),
- 'admin' => collect(APIPermission::CONST_PERMISSIONS)->except('_user')->toArray(),
+ 'admin' => ! $request->user()->root_admin ?: collect(APIPermission::CONST_PERMISSIONS)->except('_user')->toArray(),
],
]);
}
@@ -99,30 +101,25 @@ class APIController extends Controller
/**
* Handle saving new API key.
*
- * @param \Pterodactyl\Http\Requests\ApiKeyRequest $request
+ * @param \Pterodactyl\Http\Requests\Base\ApiKeyFormRequest $request
* @return \Illuminate\Http\RedirectResponse
- *
* @throws \Exception
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
- public function store(ApiKeyRequest $request)
+ public function store(ApiKeyFormRequest $request)
{
$adminPermissions = [];
- if ($request->user()->isRootAdmin()) {
+ if ($request->user()->root_admin) {
$adminPermissions = $request->input('admin_permissions') ?? [];
}
- $secret = $this->service->create([
+ $secret = $this->keyService->handle([
'user_id' => $request->user()->id,
'allowed_ips' => $request->input('allowed_ips'),
'memo' => $request->input('memo'),
- ], $request->input('permissions') ?? [], $adminPermissions);
+ ], $request->input('permissions', []), $adminPermissions);
- $this->alert->success(
- "An API Key-Pair has successfully been generated. The API secret
- for this public key is shown below and will not be shown again.
- {$secret}
"
- )->flash();
+ $this->alert->success(trans('base.api.index.keypair_created', ['token' => $secret]))->flash();
return redirect()->route('account.api');
}
@@ -136,16 +133,10 @@ class APIController extends Controller
*/
public function revoke(Request $request, $key)
{
- try {
- $key = $this->repository->withColumns('id')->findFirstWhere([
- ['user_id', '=', $request->user()->id],
- ['public', $key],
- ]);
-
- $this->service->revoke($key->id);
- } catch (RecordNotFoundException $ex) {
- return abort(404);
- }
+ $this->repository->deleteWhere([
+ ['user_id', '=', $request->user()->id],
+ ['public', '=', $key],
+ ]);
return response('', 204);
}
diff --git a/app/Http/Controllers/Base/AccountController.php b/app/Http/Controllers/Base/AccountController.php
index 0c00b92c..102850ed 100644
--- a/app/Http/Controllers/Base/AccountController.php
+++ b/app/Http/Controllers/Base/AccountController.php
@@ -25,83 +25,69 @@
namespace Pterodactyl\Http\Controllers\Base;
-use Log;
-use Alert;
-use Illuminate\Http\Request;
-use Pterodactyl\Models\User;
+use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
-use Pterodactyl\Exceptions\DisplayValidationException;
+use Pterodactyl\Http\Requests\Base\AccountDataFormRequest;
+use Pterodactyl\Services\Users\UserUpdateService;
class AccountController extends Controller
{
- public function __construct()
- {
+ /**
+ * @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.
*
- * @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
- public function index(Request $request)
+ public function index()
{
return view('base.account');
}
/**
- * Update details for a users account.
+ * Update details for a user's account.
*
- * @param \Illuminate\Http\Request $request
+ * @param \Pterodactyl\Http\Requests\Base\AccountDataFormRequest $request
* @return \Illuminate\Http\RedirectResponse
- * @throws \Symfony\Component\HttpKernel\Exception\HttpException
+ *
+ * @throws \Pterodactyl\Exceptions\Model\DataValidationException
+ * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
- public function update(Request $request)
+ public function update(AccountDataFormRequest $request)
{
$data = [];
-
- // Request to update account Password
if ($request->input('do_action') === 'password') {
- $this->validate($request, [
- 'current_password' => 'required',
- 'new_password' => 'required|confirmed|' . User::PASSWORD_RULES,
- 'new_password_confirmation' => 'required',
- ]);
-
$data['password'] = $request->input('new_password');
-
- // Request to update account Email
} elseif ($request->input('do_action') === 'email') {
$data['email'] = $request->input('new_email');
-
- // Request to update account Identity
} elseif ($request->input('do_action') === 'identity') {
$data = $request->only(['name_first', 'name_last', 'username']);
-
- // Unknown, hit em with a 404
- } else {
- return abort(404);
}
- if (
- in_array($request->input('do_action'), ['email', 'password'])
- && ! password_verify($request->input('current_password'), $request->user()->password)
- ) {
- Alert::danger(trans('base.account.invalid_pass'))->flash();
-
- return redirect()->route('account');
- }
-
- try {
- $repo = new oldUserRepository;
- $repo->update($request->user()->id, $data);
- Alert::success('Your account details were successfully updated.')->flash();
- } catch (DisplayValidationException $ex) {
- return redirect()->route('account')->withErrors(json_decode($ex->getMessage()));
- } catch (\Exception $ex) {
- Log::error($ex);
- Alert::danger(trans('base.account.exception'))->flash();
- }
+ $this->updateService->handle($request->user()->id, $data);
+ $this->alert->success(trans('base.account.details_updated'))->flash();
return redirect()->route('account');
}
diff --git a/app/Http/Controllers/Base/IndexController.php b/app/Http/Controllers/Base/IndexController.php
index e9d9e768..50416300 100644
--- a/app/Http/Controllers/Base/IndexController.php
+++ b/app/Http/Controllers/Base/IndexController.php
@@ -26,11 +26,45 @@
namespace Pterodactyl\Http\Controllers\Base;
use Illuminate\Http\Request;
-use Pterodactyl\Models\Server;
+use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Http\Controllers\Controller;
+use Pterodactyl\Services\Servers\ServerAccessHelperService;
+use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface;
class IndexController extends Controller
{
+ /**
+ * @var \Pterodactyl\Services\Servers\ServerAccessHelperService
+ */
+ protected $access;
+
+ /**
+ * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface
+ */
+ protected $daemonRepository;
+
+ /**
+ * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
+ */
+ protected $repository;
+
+ /**
+ * IndexController constructor.
+ *
+ * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository
+ * @param \Pterodactyl\Services\Servers\ServerAccessHelperService $access
+ * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
+ */
+ public function __construct(
+ DaemonServerRepositoryInterface $daemonRepository,
+ ServerAccessHelperService $access,
+ ServerRepositoryInterface $repository
+ ) {
+ $this->access = $access;
+ $this->daemonRepository = $daemonRepository;
+ $this->repository = $repository;
+ }
+
/**
* Returns listing of user's servers.
*
@@ -39,38 +73,11 @@ class IndexController extends Controller
*/
public function getIndex(Request $request)
{
- $servers = $request->user()->access()->with('user');
+ $servers = $this->repository->search($request->input('query'))->filterUserAccessServers(
+ $request->user()->id, $request->user()->root_admin, 'all', ['user']
+ );
- if (! is_null($request->input('query'))) {
- $servers->search($request->input('query'));
- }
-
- return view('base.index', [
- 'servers' => $servers->paginate(config('pterodactyl.paginate.frontend.servers')),
- ]);
- }
-
- /**
- * Generate a random string.
- *
- * @param \Illuminate\Http\Request $request
- * @param int $length
- * @return string
- * @deprecated
- */
- public function getPassword(Request $request, $length = 16)
- {
- $length = ($length < 8) ? 8 : $length;
-
- $returnable = false;
- while (! $returnable) {
- $generated = str_random($length);
- if (preg_match('/[A-Z]+[a-z]+[0-9]+/', $generated)) {
- $returnable = true;
- }
- }
-
- return $generated;
+ return view('base.index', ['servers' => $servers]);
}
/**
@@ -79,31 +86,23 @@ class IndexController extends Controller
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @return \Illuminate\Http\JsonResponse
+ * @throws \Exception
*/
public function status(Request $request, $uuid)
{
- $server = Server::byUuid($uuid);
-
- if (! $server) {
- return response()->json([], 404);
- }
+ $server = $this->access->handle($uuid, $request->user());
if (! $server->installed) {
return response()->json(['status' => 20]);
- }
-
- if ($server->suspended) {
+ } elseif ($server->suspended) {
return response()->json(['status' => 30]);
}
- try {
- $res = $server->guzzleClient()->request('GET', '/server');
- if ($res->getStatusCode() === 200) {
- return response()->json(json_decode($res->getBody()));
- }
- } catch (\Exception $e) {
- }
+ $response = $this->daemonRepository->setNode($server->node_id)
+ ->setAccessServer($server->uuid)
+ ->setAccessToken($server->daemonSecret)
+ ->details();
- return response()->json([]);
+ return response()->json(json_decode($response->getBody()));
}
}
diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php
index 5a143e65..b44e6e0f 100644
--- a/app/Http/Controllers/Base/SecurityController.php
+++ b/app/Http/Controllers/Base/SecurityController.php
@@ -25,14 +25,64 @@
namespace Pterodactyl\Http\Controllers\Base;
-use Alert;
-use Google2FA;
+use Illuminate\Contracts\Config\Repository as ConfigRepository;
+use Illuminate\Contracts\Session\Session;
use Illuminate\Http\Request;
-use Pterodactyl\Models\Session;
+use Prologue\Alerts\AlertsMessageBag;
+use Pterodactyl\Contracts\Repository\SessionRepositoryInterface;
+use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
use Pterodactyl\Http\Controllers\Controller;
+use Pterodactyl\Services\Users\ToggleTwoFactorService;
+use Pterodactyl\Services\Users\TwoFactorSetupService;
class SecurityController extends Controller
{
+ /**
+ * @var \Prologue\Alerts\AlertsMessageBag
+ */
+ protected $alert;
+
+ /**
+ * @var \Illuminate\Contracts\Config\Repository
+ */
+ protected $config;
+
+ /**
+ * @var \Pterodactyl\Contracts\Repository\SessionRepositoryInterface
+ */
+ protected $repository;
+
+ /**
+ * @var \Illuminate\Contracts\Session\Session
+ */
+ protected $session;
+
+ /**
+ * @var \Pterodactyl\Services\Users\ToggleTwoFactorService
+ */
+ protected $toggleTwoFactorService;
+
+ /**
+ * @var \Pterodactyl\Services\Users\TwoFactorSetupService
+ */
+ protected $twoFactorSetupService;
+
+ public function __construct(
+ AlertsMessageBag $alert,
+ ConfigRepository $config,
+ Session $session,
+ SessionRepositoryInterface $repository,
+ ToggleTwoFactorService $toggleTwoFactorService,
+ TwoFactorSetupService $twoFactorSetupService
+ ) {
+ $this->alert = $alert;
+ $this->config = $config;
+ $this->repository = $repository;
+ $this->session = $session;
+ $this->toggleTwoFactorService = $toggleTwoFactorService;
+ $this->twoFactorSetupService = $twoFactorSetupService;
+ }
+
/**
* Returns Security Management Page.
*
@@ -41,8 +91,12 @@ class SecurityController extends Controller
*/
public function index(Request $request)
{
+ if ($this->config->get('session.driver') === 'database') {
+ $activeSessions = $this->repository->getUserSessions($request->user()->id);
+ }
+
return view('base.security', [
- 'sessions' => Session::where('user_id', $request->user()->id)->get(),
+ 'sessions' => $activeSessions ?? null,
]);
}
@@ -52,22 +106,13 @@ class SecurityController extends Controller
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
+ *
+ * @throws \Pterodactyl\Exceptions\Model\DataValidationException
+ * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function generateTotp(Request $request)
{
- $user = $request->user();
-
- $user->totp_secret = Google2FA::generateSecretKey();
- $user->save();
-
- return response()->json([
- 'qrImage' => Google2FA::getQRCodeGoogleUrl(
- 'Pterodactyl',
- $user->email,
- $user->totp_secret
- ),
- 'secret' => $user->totp_secret,
- ]);
+ return response()->json($this->twoFactorSetupService->handle($request->user()));
}
/**
@@ -78,18 +123,13 @@ class SecurityController extends Controller
*/
public function setTotp(Request $request)
{
- if (! $request->has('token')) {
- return response()->json([
- 'error' => 'Request is missing token parameter.',
- ], 500);
- }
+ try {
+ $this->toggleTwoFactorService->handle($request->user(), $request->input('token'));
- $user = $request->user();
- if ($user->toggleTotp($request->input('token'))) {
return response('true');
+ } catch (TwoFactorAuthenticationTokenInvalid $exception) {
+ return response('false');
}
-
- return response('false');
}
/**
@@ -100,19 +140,12 @@ class SecurityController extends Controller
*/
public function disableTotp(Request $request)
{
- if (! $request->has('token')) {
- Alert::danger('Missing required `token` field in request.')->flash();
-
- return redirect()->route('account.security');
+ try {
+ $this->toggleTwoFactorService->handle($request->user(), $request->input('token'), false);
+ } catch (TwoFactorAuthenticationTokenInvalid $exception) {
+ $this->alert->danger(trans('base.security.2fa_disable_error'))->flash();
}
- $user = $request->user();
- if ($user->toggleTotp($request->input('token'))) {
- return redirect()->route('account.security');
- }
-
- Alert::danger('The TOTP token provided was invalid.')->flash();
-
return redirect()->route('account.security');
}
@@ -125,7 +158,7 @@ class SecurityController extends Controller
*/
public function revoke(Request $request, $id)
{
- Session::where('user_id', $request->user()->id)->findOrFail($id)->delete();
+ $this->repository->deleteUserSession($request->user()->id, $id);
return redirect()->route('account.security');
}
diff --git a/app/Http/Requests/Base/AccountDataFormRequest.php b/app/Http/Requests/Base/AccountDataFormRequest.php
new file mode 100644
index 00000000..a9573106
--- /dev/null
+++ b/app/Http/Requests/Base/AccountDataFormRequest.php
@@ -0,0 +1,85 @@
+.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+namespace Pterodactyl\Http\Requests\Base;
+
+use Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException;
+use Pterodactyl\Models\User;
+use Pterodactyl\Http\Requests\FrontendUserFormRequest;
+
+class AccountDataFormRequest extends FrontendUserFormRequest
+{
+ /**
+ * @return bool
+ * @throws \Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException
+ */
+ public function authorize()
+ {
+ if (! parent::authorize()) {
+ return false;
+ }
+
+ // Verify password matches when changing password or email.
+ if (in_array($this->input('do_action'), ['password', 'email'])) {
+ if (! password_verify($this->input('current_password'), $this->user()->password)) {
+ throw new InvalidPasswordProvidedException(trans('base.account.invalid_password'));
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @return array
+ */
+ public function rules()
+ {
+ $modelRules = User::getUpdateRulesForId($this->user()->id);
+
+ switch ($this->input('do_action')) {
+ case 'email':
+ $rules = [
+ 'new_email' => array_get($modelRules, 'email'),
+ ];
+ break;
+ case 'password':
+ $rules = [
+ 'new_password' => 'required|confirmed|string|min:8',
+ 'new_password_confirmation' => 'required',
+ ];
+ break;
+ case 'identity':
+ $rules = [
+ 'name_first' => array_get($modelRules, 'name_first'),
+ 'name_last' => array_get($modelRules, 'name_last'),
+ 'username' => array_get($modelRules, 'username'),
+ ];
+ break;
+ default:
+ abort(422);
+ }
+
+ return $rules;
+ }
+}
diff --git a/app/Http/Requests/ApiKeyRequest.php b/app/Http/Requests/Base/ApiKeyFormRequest.php
similarity index 91%
rename from app/Http/Requests/ApiKeyRequest.php
rename to app/Http/Requests/Base/ApiKeyFormRequest.php
index 52b8f90e..33b2541c 100644
--- a/app/Http/Requests/ApiKeyRequest.php
+++ b/app/Http/Requests/Base/ApiKeyFormRequest.php
@@ -22,11 +22,12 @@
* SOFTWARE.
*/
-namespace Pterodactyl\Http\Requests;
+namespace Pterodactyl\Http\Requests\Base;
use IPTools\Network;
+use Pterodactyl\Http\Requests\FrontendUserFormRequest;
-class ApiKeyRequest extends BaseFormRequest
+class ApiKeyFormRequest extends FrontendUserFormRequest
{
/**
* Rules applied to data passed in this request.
@@ -58,7 +59,7 @@ class ApiKeyRequest extends BaseFormRequest
}
}
- $this->merge(['allowed_ips' => $loop], $this->except('allowed_ips'));
+ $this->merge(['allowed_ips' => $loop]);
}
/**
@@ -69,12 +70,11 @@ class ApiKeyRequest extends BaseFormRequest
public function withValidator($validator)
{
$validator->after(function ($validator) {
+ /* @var \Illuminate\Validation\Validator $validator */
if (empty($this->input('permissions')) && empty($this->input('admin_permissions'))) {
$validator->errors()->add('permissions', 'At least one permission must be selected.');
}
- });
- $validator->after(function ($validator) {
foreach ($this->input('allowed_ips') as $ip) {
$ip = trim($ip);
diff --git a/app/Http/Requests/BaseFormRequest.php b/app/Http/Requests/FrontendUserFormRequest.php
similarity index 94%
rename from app/Http/Requests/BaseFormRequest.php
rename to app/Http/Requests/FrontendUserFormRequest.php
index 7d5274bb..404003c3 100644
--- a/app/Http/Requests/BaseFormRequest.php
+++ b/app/Http/Requests/FrontendUserFormRequest.php
@@ -26,8 +26,10 @@ namespace Pterodactyl\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
-class BaseFormRequest extends FormRequest
+abstract class FrontendUserFormRequest extends FormRequest
{
+ abstract public function rules();
+
/**
* Determine if a user is authorized to access this endpoint.
*
diff --git a/app/Models/User.php b/app/Models/User.php
index d9f99d01..a3493522 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -50,21 +50,6 @@ class User extends Model implements
{
use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable;
- /**
- * The rules for user passwords.
- *
- * @var string
- * @deprecated
- */
- const PASSWORD_RULES = 'regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})';
-
- /**
- * The regex rules for usernames.
- *
- * @var string
- */
- const USERNAME_RULES = 'regex:/^([\w\d\.\-]{1,255})$/';
-
/**
* Level of servers to display when using access() on a user.
*
@@ -92,9 +77,9 @@ class User extends Model implements
* @var array
*/
protected $casts = [
- 'root_admin' => 'integer',
- 'use_totp' => 'integer',
- 'gravatar' => 'integer',
+ 'root_admin' => 'boolean',
+ 'use_totp' => 'boolean',
+ 'gravatar' => 'boolean',
];
/**
@@ -135,11 +120,11 @@ class User extends Model implements
* @var array
*/
protected static $applicationRules = [
- 'email' => 'required|email',
- 'username' => 'required|alpha_dash',
- 'name_first' => 'required|string',
- 'name_last' => 'required|string',
- 'password' => 'sometimes|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})',
+ 'email' => 'required',
+ 'username' => 'required',
+ 'name_first' => 'required',
+ 'name_last' => 'required',
+ 'password' => 'sometimes',
];
/**
@@ -148,10 +133,10 @@ class User extends Model implements
* @var array
*/
protected static $dataIntegrityRules = [
- 'email' => 'unique:users,email',
- 'username' => 'between:1,255|unique:users,username',
- 'name_first' => 'between:1,255',
- 'name_last' => 'between:1,255',
+ 'email' => 'email|unique:users,email',
+ 'username' => 'alpha_dash|between:1,255|unique:users,username',
+ 'name_first' => 'string|between:1,255',
+ 'name_last' => 'string|between:1,255',
'password' => 'nullable|string',
'root_admin' => 'boolean',
'language' => 'string|between:2,5',
diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php
index a0a85fae..d30ae322 100644
--- a/app/Providers/RepositoryServiceProvider.php
+++ b/app/Providers/RepositoryServiceProvider.php
@@ -25,10 +25,16 @@
namespace Pterodactyl\Providers;
use Illuminate\Support\ServiceProvider;
+use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface;
+use Pterodactyl\Contracts\Repository\SessionRepositoryInterface;
+use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Pterodactyl\Repositories\Daemon\FileRepository;
use Pterodactyl\Repositories\Daemon\PowerRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\PackRepository;
+use Pterodactyl\Repositories\Eloquent\PermissionRepository;
+use Pterodactyl\Repositories\Eloquent\SessionRepository;
+use Pterodactyl\Repositories\Eloquent\SubuserRepository;
use Pterodactyl\Repositories\Eloquent\UserRepository;
use Pterodactyl\Repositories\Daemon\CommandRepository;
use Pterodactyl\Repositories\Eloquent\ApiKeyRepository;
@@ -83,11 +89,14 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(NodeRepositoryInterface::class, NodeRepository::class);
$this->app->bind(OptionVariableRepositoryInterface::class, OptionVariableRepository::class);
$this->app->bind(PackRepositoryInterface::class, PackRepository::class);
+ $this->app->bind(PermissionRepositoryInterface::class, PermissionRepository::class);
$this->app->bind(ServerRepositoryInterface::class, ServerRepository::class);
$this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class);
$this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class);
$this->app->bind(ServiceOptionRepositoryInterface::class, ServiceOptionRepository::class);
$this->app->bind(ServiceVariableRepositoryInterface::class, ServiceVariableRepository::class);
+ $this->app->bind(SessionRepositoryInterface::class, SessionRepository::class);
+ $this->app->bind(SubuserRepositoryInterface::class, SubuserRepository::class);
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
// Daemon Repositories
diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php
index 594bb175..db2f31e6 100644
--- a/app/Repositories/Daemon/ServerRepository.php
+++ b/app/Repositories/Daemon/ServerRepository.php
@@ -161,4 +161,12 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa
{
return $this->getHttpClient()->request('DELETE', '/servers');
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function details()
+ {
+ return $this->getHttpClient()->request('GET', '/servers');
+ }
}
diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php
index 05ba8039..ec89052b 100644
--- a/app/Repositories/Eloquent/ServerRepository.php
+++ b/app/Repositories/Eloquent/ServerRepository.php
@@ -28,6 +28,7 @@ use Pterodactyl\Models\Server;
use Pterodactyl\Repositories\Concerns\Searchable;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
+use Webmozart\Assert\Assert;
class ServerRepository extends EloquentRepository implements ServerRepositoryInterface
{
@@ -149,4 +150,73 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
'pack' => (! is_null($instance->pack_id)) ? $instance->pack->uuid : null,
];
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUserAccessServers($user)
+ {
+ Assert::numeric($user, 'First argument passed to getUserAccessServers must be numeric, received %s.');
+
+ $subuser = $this->app->make(SubuserRepository::class);
+
+ return $this->getBuilder()->select('id')->where('owner_id', $user)->union(
+ $subuser->getBuilder()->select('server_id')->where('user_id', $user)
+ )->pluck('id')->all();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filterUserAccessServers($user, $admin = false, $level = 'all', array $relations = [])
+ {
+ Assert::numeric($user, 'First argument passed to filterUserAccessServers must be numeric, received %s.');
+ Assert::boolean($admin, 'Second argument passed to filterUserAccessServers must be boolean, received %s.');
+ Assert::stringNotEmpty($level, 'Third argument passed to filterUserAccessServers must be a non-empty string, received %s.');
+
+ $instance = $this->getBuilder()->with($relations);
+
+ // If access level is set to owner, only display servers
+ // that the user owns.
+ if ($level === 'owner') {
+ $instance->where('owner_id', $user);
+ }
+
+ // If set to all, display all servers they can access, including
+ // those they access as an admin.
+ //
+ // If set to subuser, only return the servers they can access because
+ // they are owner, or marked as a subuser of the server.
+ if (($level === 'all' && ! $admin) || $level === 'subuser') {
+ $instance->whereIn('id', $this->getUserAccessServers($user));
+ }
+
+ // If set to admin, only display the servers a user can access
+ // as an administrator (leaves out owned and subuser of).
+ if ($level === 'admin' && $admin) {
+ $instance->whereIn('id', $this->getUserAccessServers($user));
+ }
+
+ return $instance->search($this->searchTerm)->paginate(
+ $this->app->make('config')->get('pterodactyl.paginate.frontend.servers')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getByUuid($uuid)
+ {
+ Assert::stringNotEmpty($uuid, 'First argument passed to getByUuid must be a non-empty string, received %s.');
+
+ $instance = $this->getBuilder()->with('service', 'node')->where(function ($query) use ($uuid) {
+ $query->where('uuidShort', $uuid)->orWhere('uuid', $uuid);
+ })->first($this->getColumns());
+
+ if (! $instance) {
+ throw new RecordNotFoundException;
+ }
+
+ return $instance;
+ }
}
diff --git a/app/Repositories/Eloquent/SessionRepository.php b/app/Repositories/Eloquent/SessionRepository.php
new file mode 100644
index 00000000..928feb56
--- /dev/null
+++ b/app/Repositories/Eloquent/SessionRepository.php
@@ -0,0 +1,55 @@
+.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+namespace Pterodactyl\Repositories\Eloquent;
+
+use Pterodactyl\Models\Session;
+use Pterodactyl\Contracts\Repository\SessionRepositoryInterface;
+
+class SessionRepository extends EloquentRepository implements SessionRepositoryInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function model()
+ {
+ return Session::class;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUserSessions($user)
+ {
+ return $this->getBuilder()->where('user_id', $user)->get($this->getColumns());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteUserSession($user, $session)
+ {
+ return $this->getBuilder()->where('user_id', $user)->where('id', $session)->delete();
+ }
+}
diff --git a/app/Services/Api/KeyService.php b/app/Services/Api/KeyCreationService.php
similarity index 89%
rename from app/Services/Api/KeyService.php
rename to app/Services/Api/KeyCreationService.php
index fc67b892..4beaf3a4 100644
--- a/app/Services/Api/KeyService.php
+++ b/app/Services/Api/KeyCreationService.php
@@ -28,7 +28,7 @@ use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
-class KeyService
+class KeyCreationService
{
const PUB_CRYPTO_BYTES = 8;
const PRIV_CRYPTO_BYTES = 32;
@@ -36,7 +36,7 @@ class KeyService
/**
* @var \Illuminate\Database\ConnectionInterface
*/
- protected $database;
+ protected $connection;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
@@ -57,18 +57,18 @@ class KeyService
* ApiKeyService constructor.
*
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
- * @param \Illuminate\Database\ConnectionInterface $database
+ * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \Pterodactyl\Services\Api\PermissionService $permissionService
*/
public function __construct(
ApiKeyRepositoryInterface $repository,
- ConnectionInterface $database,
+ ConnectionInterface $connection,
Encrypter $encrypter,
PermissionService $permissionService
) {
$this->repository = $repository;
- $this->database = $database;
+ $this->connection = $connection;
$this->encrypter = $encrypter;
$this->permissionService = $permissionService;
}
@@ -84,13 +84,13 @@ class KeyService
* @throws \Exception
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
- public function create(array $data, array $permissions, array $administrative = [])
+ public function handle(array $data, array $permissions, array $administrative = [])
{
$publicKey = bin2hex(random_bytes(self::PUB_CRYPTO_BYTES));
$secretKey = bin2hex(random_bytes(self::PRIV_CRYPTO_BYTES));
// Start a Transaction
- $this->database->beginTransaction();
+ $this->connection->beginTransaction();
$data = array_merge($data, [
'public' => $publicKey,
@@ -128,19 +128,8 @@ class KeyService
$this->permissionService->create($instance->id, $permission);
}
- $this->database->commit();
+ $this->connection->commit();
return $secretKey;
}
-
- /**
- * Delete the API key and associated permissions from the database.
- *
- * @param int $id
- * @return bool|null
- */
- public function revoke($id)
- {
- return $this->repository->delete($id);
- }
}
diff --git a/app/Services/Servers/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php
new file mode 100644
index 00000000..4ee77012
--- /dev/null
+++ b/app/Services/Servers/ServerAccessHelperService.php
@@ -0,0 +1,71 @@
+.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+namespace Pterodactyl\Services\Servers;
+
+use Illuminate\Cache\Repository as CacheRepository;
+use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
+use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
+use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
+use Pterodactyl\Models\User;
+
+class ServerAccessHelperService
+{
+ public function __construct(
+ CacheRepository $cache,
+ ServerRepositoryInterface $repository,
+ SubuserRepositoryInterface $subuserRepository,
+ UserRepositoryInterface $userRepository
+ ) {
+ $this->cache = $cache;
+ $this->repository = $repository;
+ $this->subuserRepository = $subuserRepository;
+ $this->userRepository = $userRepository;
+ }
+
+ public function handle($uuid, $user)
+ {
+ if (! $user instanceof User) {
+ $user = $this->userRepository->find($user);
+ }
+
+ $server = $this->repository->getByUuid($uuid);
+ if (! $user->root_admin) {
+ if (! in_array($server->id, $this->repository->getUserAccessServers($user->id))) {
+ throw new \Exception('User does not have access.');
+ }
+
+ if ($server->owner_id !== $user->id) {
+ $subuser = $this->subuserRepository->withColumns('daemonSecret')->findWhere([
+ ['user_id', '=', $user->id],
+ ['server_id', '=', $server->id],
+ ]);
+
+ $server->daemonSecret = $subuser->daemonToken;
+ }
+ }
+
+ return $server;
+ }
+}
diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php
new file mode 100644
index 00000000..f731c13b
--- /dev/null
+++ b/app/Services/Users/ToggleTwoFactorService.php
@@ -0,0 +1,84 @@
+.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+namespace Pterodactyl\Services\Users;
+
+use PragmaRX\Google2FA\Contracts\Google2FA;
+use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
+use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
+use Pterodactyl\Models\User;
+
+class ToggleTwoFactorService
+{
+ /**
+ * @var \PragmaRX\Google2FA\Contracts\Google2FA
+ */
+ protected $google2FA;
+
+ /**
+ * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
+ */
+ protected $repository;
+
+ /**
+ * ToggleTwoFactorService constructor.
+ *
+ * @param \PragmaRX\Google2FA\Contracts\Google2FA $google2FA
+ * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
+ */
+ public function __construct(
+ Google2FA $google2FA,
+ UserRepositoryInterface $repository
+ ) {
+ $this->google2FA = $google2FA;
+ $this->repository = $repository;
+ }
+
+ /**
+ * @param int|\Pterodactyl\Models\User $user
+ * @param string $token
+ * @param null|bool $toggleState
+ * @return bool
+ *
+ * @throws \Pterodactyl\Exceptions\Model\DataValidationException
+ * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
+ * @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
+ */
+ public function handle($user, $token, $toggleState = null)
+ {
+ if (! $user instanceof User) {
+ $user = $this->repository->find($user);
+ }
+
+ if (! $this->google2FA->verifyKey($user->totp_secret, $token, 2)) {
+ throw new TwoFactorAuthenticationTokenInvalid;
+ }
+
+ $this->repository->withoutFresh()->update($user->id, [
+ 'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState),
+ ]);
+
+ return true;
+ }
+}
diff --git a/app/Services/Users/TwoFactorSetupService.php b/app/Services/Users/TwoFactorSetupService.php
new file mode 100644
index 00000000..d959ef6a
--- /dev/null
+++ b/app/Services/Users/TwoFactorSetupService.php
@@ -0,0 +1,91 @@
+.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+namespace Pterodactyl\Services\Users;
+
+use Illuminate\Contracts\Config\Repository as ConfigRepository;
+use PragmaRX\Google2FA\Contracts\Google2FA;
+use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
+use Pterodactyl\Models\User;
+
+class TwoFactorSetupService
+{
+ /**
+ * @var \Illuminate\Contracts\Config\Repository
+ */
+ protected $config;
+
+ /**
+ * @var \PragmaRX\Google2FA\Contracts\Google2FA
+ */
+ protected $google2FA;
+
+ /**
+ * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
+ */
+ protected $repository;
+
+ /**
+ * TwoFactorSetupService constructor.
+ *
+ * @param \Illuminate\Contracts\Config\Repository $config
+ * @param \PragmaRX\Google2FA\Contracts\Google2FA $google2FA
+ * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
+ */
+ public function __construct(
+ ConfigRepository $config,
+ Google2FA $google2FA,
+ UserRepositoryInterface $repository
+ ) {
+ $this->config = $config;
+ $this->google2FA = $google2FA;
+ $this->repository = $repository;
+ }
+
+ /**
+ * Generate a 2FA token and store it in the database.
+ *
+ * @param int|\Pterodactyl\Models\User $user
+ * @return array
+ *
+ * @throws \Pterodactyl\Exceptions\Model\DataValidationException
+ * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
+ */
+ public function handle($user)
+ {
+ if (! $user instanceof User) {
+ $user = $this->repository->find($user);
+ }
+
+ $secret = $this->google2FA->generateSecretKey();
+ $image = $this->google2FA->getQRCodeGoogleUrl($this->config->get('app.name'), $user->email, $secret);
+
+ $this->repository->withoutFresh()->update($user->id, ['totp_secret' => $secret]);
+
+ return [
+ 'qrImage' => $image,
+ 'secret' => $secret,
+ ];
+ }
+}
diff --git a/app/Services/Users/UserUpdateService.php b/app/Services/Users/UserUpdateService.php
index 646b1940..99ce6366 100644
--- a/app/Services/Users/UserUpdateService.php
+++ b/app/Services/Users/UserUpdateService.php
@@ -61,6 +61,7 @@ class UserUpdateService
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
+ * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle($id, array $data)
{
diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php
index 2c5242df..9c7bd9d0 100644
--- a/resources/lang/en/base.php
+++ b/resources/lang/en/base.php
@@ -33,6 +33,7 @@ return [
'header_sub' => 'Manage your API access keys.',
'list' => 'API Keys',
'create_new' => 'Create New API key',
+ 'keypair_created' => 'An API Key-Pair has been generated. Your API secret token is :token
. Please take note of this key as it will not be displayed again.',
],
'new' => [
'header' => 'New API Key',
@@ -207,6 +208,8 @@ 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',
@@ -219,10 +222,9 @@ return [
'last_name' => 'Last Name',
'update_identitity' => 'Update Identity',
'username_help' => 'Your username must be unique to your account, and may only contain the following characters: :requirements.',
- 'invalid_pass' => 'The password provided was not valid for this account.',
- 'exception' => 'An error occurred while attempting to update your account.',
],
'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',
@@ -234,5 +236,6 @@ return [
'enable_2fa' => 'Enable 2-Factor Authentication',
'2fa_qr' => 'Confgure 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.',
],
];
diff --git a/resources/themes/pterodactyl/base/security.blade.php b/resources/themes/pterodactyl/base/security.blade.php
index 666a790b..8f3908db 100644
--- a/resources/themes/pterodactyl/base/security.blade.php
+++ b/resources/themes/pterodactyl/base/security.blade.php
@@ -39,30 +39,36 @@
@lang('strings.id') | -@lang('strings.ip') | -@lang('strings.last_activity') | -- |
---|
{{ substr($session->id, 0, 6) }} |
- {{ $session->ip_address }} | -{{ Carbon::createFromTimestamp($session->last_activity)->diffForHumans() }} | -- - - - | +@lang('strings.id') | +@lang('strings.ip') | +@lang('strings.last_activity') | +
---|
{{ substr($session->id, 0, 6) }}
@lang('base.security.session_mgmt_disabled')
+