diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php index 3bb85a0b..8b597141 100644 --- a/app/Http/Controllers/Base/SecurityController.php +++ b/app/Http/Controllers/Base/SecurityController.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; +use Illuminate\Http\JsonResponse; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Users\TwoFactorSetupService; @@ -62,36 +63,28 @@ class SecurityController extends Controller } /** - * Returns Security Management Page. - * - * @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. + * 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\Http\JsonResponse - * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function generateTotp(Request $request) + public function index(Request $request): JsonResponse { - return response()->json([ - 'qrImage' => $this->twoFactorSetupService->handle($request->user()), + if ($request->user()->use_totp) { + 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. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function setTotp(Request $request) + public function store(Request $request): JsonResponse { try { $this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? ''); - - return response('true'); } catch (TwoFactorAuthenticationTokenInvalid $exception) { - return response('false'); + $error = true; } + + return JsonResponse::create([ + 'success' => ! isset($error), + ]); } /** * Disables TOTP on an account. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\JsonResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function disableTotp(Request $request) + public function delete(Request $request): JsonResponse { try { $this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? '', false); } catch (TwoFactorAuthenticationTokenInvalid $exception) { - $this->alert->danger(trans('base.security.2fa_disable_error'))->flash(); + $error = true; } - return redirect()->route('account.security'); - } - - /** - * 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'); + return JsonResponse::create([ + 'success' => ! isset($error), + ]); } } diff --git a/app/Services/Users/TwoFactorSetupService.php b/app/Services/Users/TwoFactorSetupService.php index 4d2ecff8..6c8de157 100644 --- a/app/Services/Users/TwoFactorSetupService.php +++ b/app/Services/Users/TwoFactorSetupService.php @@ -11,17 +11,12 @@ namespace Pterodactyl\Services\Users; use Pterodactyl\Models\User; use PragmaRX\Google2FA\Google2FA; +use Illuminate\Support\Collection; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Illuminate\Contracts\Config\Repository as ConfigRepository; class TwoFactorSetupService { - /** - * @var \Illuminate\Contracts\Config\Repository - */ - private $config; - /** * @var \Illuminate\Contracts\Encryption\Encrypter */ @@ -40,18 +35,15 @@ class TwoFactorSetupService /** * TwoFactorSetupService constructor. * - * @param \Illuminate\Contracts\Config\Repository $config * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \PragmaRX\Google2FA\Google2FA $google2FA * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( - ConfigRepository $config, Encrypter $encrypter, Google2FA $google2FA, UserRepositoryInterface $repository ) { - $this->config = $config; $this->encrypter = $encrypter; $this->google2FA = $google2FA; $this->repository = $repository; @@ -62,20 +54,23 @@ class TwoFactorSetupService * QR code image. * * @param \Pterodactyl\Models\User $user - * @return string + * @return \Illuminate\Support\Collection * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @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')); - $image = $this->google2FA->getQRCodeGoogleUrl($this->config->get('app.name'), $user->email, $secret); + $secret = $this->google2FA->generateSecretKey(config('pterodactyl.auth.2fa.bytes')); + $image = $this->google2FA->getQRCodeGoogleUrl(config('app.name'), $user->email, $secret); $this->repository->withoutFreshModel()->update($user->id, [ 'totp_secret' => $this->encrypter->encrypt($secret), ]); - return $image; + return new Collection([ + 'image' => $image, + 'secret' => $secret, + ]); } } diff --git a/resources/assets/scripts/components/dashboard/Account.vue b/resources/assets/scripts/components/dashboard/Account.vue index c40a63b4..0f2d882b 100644 --- a/resources/assets/scripts/components/dashboard/Account.vue +++ b/resources/assets/scripts/components/dashboard/Account.vue @@ -3,7 +3,7 @@
- +
@@ -11,7 +11,7 @@
- +
@@ -39,5 +39,11 @@ modalVisible: false, }; }, + methods: { + openModal: function () { + this.$data.modalVisible = true; + window.events.$emit('two_factor:open'); + }, + } }; diff --git a/resources/assets/scripts/components/dashboard/account/ChangePassword.vue b/resources/assets/scripts/components/dashboard/account/ChangePassword.vue index d5793a07..02b2f2e7 100644 --- a/resources/assets/scripts/components/dashboard/account/ChangePassword.vue +++ b/resources/assets/scripts/components/dashboard/account/ChangePassword.vue @@ -2,26 +2,26 @@
-

Change your password

+

{{ $t('dashboard.account.password.title') }}

- +
- +

{{ errors.first('password') }}

-

Your new password should be at least 8 characters in length.

+

{{ $t('dashboard.account.password.requirements') }}

- + {{ errors.first('password_confirmation') }}

- +
@@ -68,7 +68,7 @@ this.$data.newPassword = ''; this.$data.confirmNew = ''; - this.success('Your password has been updated.'); + this.success(this.$t('dashboard.account.password.updated')); }) .catch(err => { if (!err.response) { diff --git a/resources/assets/scripts/components/dashboard/account/TwoFactorAuthentication.vue b/resources/assets/scripts/components/dashboard/account/TwoFactorAuthentication.vue index 6899e305..a6b8b38d 100644 --- a/resources/assets/scripts/components/dashboard/account/TwoFactorAuthentication.vue +++ b/resources/assets/scripts/components/dashboard/account/TwoFactorAuthentication.vue @@ -1,11 +1,191 @@ diff --git a/resources/assets/scripts/components/dashboard/account/UpdateEmail.vue b/resources/assets/scripts/components/dashboard/account/UpdateEmail.vue index 27a61b8d..b3e0de47 100644 --- a/resources/assets/scripts/components/dashboard/account/UpdateEmail.vue +++ b/resources/assets/scripts/components/dashboard/account/UpdateEmail.vue @@ -2,9 +2,9 @@
-

Update your email

+

{{ $t('dashboard.account.email.title') }}

- + {{ errors.first('email') }}

- +
- +
@@ -57,7 +57,7 @@ this.$data.password = ''; }) .then(() => { - this.success('Your email address has been updated.'); + this.success(this.$t('dashboard.account.email.updated')); }) .catch(error => { if (!error.response) { @@ -79,7 +79,3 @@ } }; - - diff --git a/resources/assets/styles/components/buttons.css b/resources/assets/styles/components/buttons.css index 733efc0a..085d5d06 100644 --- a/resources/assets/styles/components/buttons.css +++ b/resources/assets/styles/components/buttons.css @@ -20,6 +20,14 @@ } } + &.btn-red { + @apply .bg-red .border-red-dark .border .text-white; + + &:hover:enabled { + @apply .bg-red-dark .border-red-darker; + } + } + &.btn-secondary { @apply .border .border-grey-light .text-grey-dark; diff --git a/resources/assets/styles/components/forms.css b/resources/assets/styles/components/forms.css index ee934c59..85fe6cf6 100644 --- a/resources/assets/styles/components/forms.css +++ b/resources/assets/styles/components/forms.css @@ -2,6 +2,16 @@ 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 * is input and then sinks back down into the field if left empty. diff --git a/resources/assets/styles/components/spinners.css b/resources/assets/styles/components/spinners.css index b509c5a4..d55c8e35 100644 --- a/resources/assets/styles/components/spinners.css +++ b/resources/assets/styles/components/spinners.css @@ -31,11 +31,11 @@ /** * Spinner Colors */ - &.blue:after { + &.blue:after, &.text-blue:after { @apply .border-blue; } - &.white:after { + &.white:after, &.text-white:after { @apply .border-white; } diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index 01ac79b1..2646dc4f 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -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.', - ], ]; diff --git a/resources/lang/en/dashboard/account.php b/resources/lang/en/dashboard/account.php new file mode 100644 index 00000000..85411ef6 --- /dev/null +++ b/resources/lang/en/dashboard/account.php @@ -0,0 +1,28 @@ + [ + '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', + ], + ], +]; diff --git a/resources/lang/en/strings.php b/resources/lang/en/strings.php index ebdd60d2..c9fbb634 100644 --- a/resources/lang/en/strings.php +++ b/resources/lang/en/strings.php @@ -2,9 +2,11 @@ return [ 'email' => 'Email', + 'email_address' => 'Email address', 'user_identifier' => 'Username or Email', 'password' => 'Password', - 'confirm_password' => 'Confirm Password', + 'new_password' => 'New password', + 'confirm_password' => 'Confirm new password', 'login' => 'Login', 'home' => 'Home', 'servers' => 'Servers', @@ -85,7 +87,8 @@ return [ 'sat' => 'Saturday', ], 'last_used' => 'Last Used', - - // Copyright Line + 'enable' => 'Enable', + 'disable' => 'Disable', + 'save' => 'Save', 'copyright' => '© 2015 - :year Pterodactyl Software', ]; diff --git a/routes/base.php b/routes/base.php index 4c8faa33..3dc1aa67 100644 --- a/routes/base.php +++ b/routes/base.php @@ -1,11 +1,5 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ + Route::get('/', 'IndexController@index')->name('index'); /* @@ -16,11 +10,6 @@ Route::get('/', 'IndexController@index')->name('index'); | Endpoint: /account | */ -//Route::group(['prefix' => 'account'], function () { -// Route::get('/', 'AccountController@index')->name('account'); -// -// Route::post('/', 'AccountController@update'); -//}); /* |-------------------------------------------------------------------------- @@ -47,15 +36,10 @@ Route::group(['prefix' => 'account/api'], function () { | Endpoint: /account/security | */ -Route::group(['prefix' => 'account/security'], function () { - Route::get('/', 'SecurityController@index')->name('account.security'); - Route::get('/revoke/{id}', 'SecurityController@revoke')->name('account.security.revoke'); - - Route::put('/totp', 'SecurityController@generateTotp')->name('account.security.totp'); - - Route::post('/totp', 'SecurityController@setTotp')->name('account.security.totp.set'); - - Route::delete('/totp', 'SecurityController@disableTotp')->name('account.security.totp.disable'); +Route::group(['prefix' => 'account/two_factor'], function () { + Route::get('/', 'SecurityController@index')->name('account.two_factor'); + Route::post('/totp', 'SecurityController@store')->name('account.two_factor.enable'); + Route::post('/totp/disable', 'SecurityController@delete')->name('account.two_factor.disable'); }); // Catch any other combinations of routes and pass them off to the Vuejs component.