Skip to content

Commit eb39826

Browse files
committed
Add base logic to configure two factor on account
1 parent edf27a5 commit eb39826

File tree

15 files changed

+384
-49
lines changed

15 files changed

+384
-49
lines changed
Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
<?php
2-
/**
3-
* Pterodactyl - Panel
4-
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
5-
*
6-
* This software is licensed under the terms of the MIT license.
7-
* https://opensource.org/licenses/MIT
8-
*/
92

103
namespace Pterodactyl\Exceptions\Service\User;
114

12-
use Exception;
5+
use Pterodactyl\Exceptions\DisplayException;
136

14-
class TwoFactorAuthenticationTokenInvalid extends Exception
7+
class TwoFactorAuthenticationTokenInvalid extends DisplayException
158
{
169
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Controllers\Api\Client;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Http\Response;
7+
use Illuminate\Http\JsonResponse;
8+
use Illuminate\Contracts\Validation\Factory;
9+
use Illuminate\Validation\ValidationException;
10+
use Pterodactyl\Services\Users\TwoFactorSetupService;
11+
use Pterodactyl\Services\Users\ToggleTwoFactorService;
12+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
13+
14+
class TwoFactorController extends ClientApiController
15+
{
16+
/**
17+
* @var \Pterodactyl\Services\Users\TwoFactorSetupService
18+
*/
19+
private $setupService;
20+
21+
/**
22+
* @var \Illuminate\Contracts\Validation\Factory
23+
*/
24+
private $validation;
25+
26+
/**
27+
* @var \Pterodactyl\Services\Users\ToggleTwoFactorService
28+
*/
29+
private $toggleTwoFactorService;
30+
31+
/**
32+
* TwoFactorController constructor.
33+
*
34+
* @param \Pterodactyl\Services\Users\ToggleTwoFactorService $toggleTwoFactorService
35+
* @param \Pterodactyl\Services\Users\TwoFactorSetupService $setupService
36+
* @param \Illuminate\Contracts\Validation\Factory $validation
37+
*/
38+
public function __construct(
39+
ToggleTwoFactorService $toggleTwoFactorService,
40+
TwoFactorSetupService $setupService,
41+
Factory $validation
42+
) {
43+
parent::__construct();
44+
45+
$this->setupService = $setupService;
46+
$this->validation = $validation;
47+
$this->toggleTwoFactorService = $toggleTwoFactorService;
48+
}
49+
50+
/**
51+
* Returns two-factor token credentials that allow a user to configure
52+
* it on their account. If two-factor is already enabled this endpoint
53+
* will return a 400 error.
54+
*
55+
* @param \Illuminate\Http\Request $request
56+
* @return \Illuminate\Http\JsonResponse
57+
*
58+
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
59+
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
60+
*/
61+
public function index(Request $request)
62+
{
63+
if ($request->user()->totp_enabled) {
64+
throw new BadRequestHttpException('Two-factor authentication is already enabled on this account.');
65+
}
66+
67+
return JsonResponse::create([
68+
'data' => [
69+
'image_url_data' => $this->setupService->handle($request->user()),
70+
],
71+
]);
72+
}
73+
74+
/**
75+
* Updates a user's account to have two-factor enabled.
76+
*
77+
* @param \Illuminate\Http\Request $request
78+
* @return \Illuminate\Http\JsonResponse
79+
*
80+
* @throws \Illuminate\Validation\ValidationException
81+
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
82+
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
83+
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
84+
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
85+
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
86+
* @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
87+
*/
88+
public function store(Request $request)
89+
{
90+
$validator = $this->validation->make($request->all(), [
91+
'code' => 'required|string',
92+
]);
93+
94+
if ($validator->fails()) {
95+
throw new ValidationException($validator);
96+
}
97+
98+
$this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true);
99+
100+
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
101+
}
102+
103+
public function delete()
104+
{
105+
}
106+
}

app/Services/Users/ToggleTwoFactorService.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ public function handle(User $user, string $token, bool $toggleState = null): boo
6565
$isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window'));
6666

6767
if (! $isValidToken) {
68-
throw new TwoFactorAuthenticationTokenInvalid;
68+
throw new TwoFactorAuthenticationTokenInvalid(
69+
'The token provided is not valid.'
70+
);
6971
}
7072

7173
$this->repository->withoutFreshModel()->update($user->id, [

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"react-transition-group": "^4.3.0",
3333
"sockette": "^2.0.6",
3434
"styled-components": "^4.4.1",
35+
"styled-components-breakpoint": "^3.0.0-preview.20",
3536
"use-react-router": "^1.0.7",
3637
"uuid": "^3.3.2",
3738
"xterm": "^3.14.4",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import http from '@/api/http';
2+
3+
export default (code: string): Promise<void> => {
4+
return new Promise((resolve, reject) => {
5+
http.post('/api/client/account/two-factor', { code })
6+
.then(() => resolve())
7+
.catch(reject);
8+
});
9+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import http from '@/api/http';
2+
3+
export default (): Promise<string> => {
4+
return new Promise((resolve, reject) => {
5+
http.get('/api/client/account/two-factor')
6+
.then(({ data }) => resolve(data.data.image_url_data))
7+
.catch(reject);
8+
});
9+
};

resources/scripts/components/App.tsx

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import ServerRouter from '@/routers/ServerRouter';
88
import AuthenticationRouter from '@/routers/AuthenticationRouter';
99
import { Provider } from 'react-redux';
1010
import { SiteSettings } from '@/state/settings';
11+
import { DefaultTheme, ThemeProvider } from 'styled-components';
1112

1213
interface ExtendedWindow extends Window {
1314
SiteConfiguration?: SiteSettings;
@@ -23,6 +24,16 @@ interface ExtendedWindow extends Window {
2324
};
2425
}
2526

27+
const theme: DefaultTheme = {
28+
breakpoints: {
29+
xs: 0,
30+
sm: 576,
31+
md: 768,
32+
lg: 992,
33+
xl: 1200,
34+
},
35+
};
36+
2637
const App = () => {
2738
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
2839
if (PterodactylUser && !store.getState().user.data) {
@@ -43,21 +54,23 @@ const App = () => {
4354
}
4455

4556
return (
46-
<StoreProvider store={store}>
47-
<Provider store={store}>
48-
<Router basename={'/'}>
49-
<div className={'mx-auto w-auto'}>
50-
<BrowserRouter basename={'/'}>
51-
<Switch>
52-
<Route path="/server/:id" component={ServerRouter}/>
53-
<Route path="/auth" component={AuthenticationRouter}/>
54-
<Route path="/" component={DashboardRouter}/>
55-
</Switch>
56-
</BrowserRouter>
57-
</div>
58-
</Router>
59-
</Provider>
60-
</StoreProvider>
57+
<ThemeProvider theme={theme}>
58+
<StoreProvider store={store}>
59+
<Provider store={store}>
60+
<Router basename={'/'}>
61+
<div className={'mx-auto w-auto'}>
62+
<BrowserRouter basename={'/'}>
63+
<Switch>
64+
<Route path="/server/:id" component={ServerRouter}/>
65+
<Route path="/auth" component={AuthenticationRouter}/>
66+
<Route path="/" component={DashboardRouter}/>
67+
</Switch>
68+
</BrowserRouter>
69+
</div>
70+
</Router>
71+
</Provider>
72+
</StoreProvider>
73+
</ThemeProvider>
6174
);
6275
};
6376

resources/scripts/components/dashboard/AccountOverviewContainer.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,38 @@ import * as React from 'react';
22
import ContentBox from '@/components/elements/ContentBox';
33
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
44
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
5+
import ConfigureTwoFactorForm from '@/components/dashboard/forms/ConfigureTwoFactorForm';
6+
import styled from 'styled-components';
7+
import { breakpoint } from 'styled-components-breakpoint';
8+
9+
const Container = styled.div`
10+
${tw`flex flex-wrap my-10`};
11+
12+
& > div {
13+
${tw`w-full`};
14+
15+
${breakpoint('md')`
16+
width: calc(50% - 1rem);
17+
`}
18+
19+
${breakpoint('xl')`
20+
${tw`w-auto flex-1`};
21+
`}
22+
}
23+
`;
524

625
export default () => {
726
return (
8-
<div className={'flex my-10'}>
9-
<ContentBox className={'flex-1 mr-4'} title={'Update Password'} showFlashes={'account:password'}>
27+
<Container>
28+
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
1029
<UpdatePasswordForm/>
1130
</ContentBox>
12-
<ContentBox className={'flex-1 ml-4'} title={'Update Email Address'} showFlashes={'account:email'}>
31+
<ContentBox className={'mt-8 md:mt-0 md:ml-8'} title={'Update Email Address'} showFlashes={'account:email'}>
1332
<UpdateEmailAddressForm/>
1433
</ContentBox>
15-
</div>
34+
<ContentBox className={'xl:ml-8 mt-8 xl:mt-0'} title={'Configure Two Factor'}>
35+
<ConfigureTwoFactorForm/>
36+
</ContentBox>
37+
</Container>
1638
);
1739
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { useState } from 'react';
2+
import { useStoreState } from 'easy-peasy';
3+
import { ApplicationStore } from '@/state';
4+
import SetupTwoFactorModal from '@/components/dashboard/forms/SetupTwoFactorModal';
5+
6+
export default () => {
7+
const user = useStoreState((state: ApplicationStore) => state.user.data!);
8+
const [visible, setVisible] = useState(false);
9+
10+
return user.useTotp ?
11+
<div>
12+
<p className={'text-sm'}>
13+
Two-factor authentication is currently enabled on your account.
14+
</p>
15+
<div className={'mt-6'}>
16+
<button className={'btn btn-red btn-secondary btn-sm'}>
17+
Disable
18+
</button>
19+
</div>
20+
</div>
21+
:
22+
<div>
23+
<SetupTwoFactorModal visible={visible} onDismissed={() => setVisible(false)}/>
24+
<p className={'text-sm'}>
25+
You do not currently have two-factor authentication enabled on your account. Click
26+
the button below to begin configuring it.
27+
</p>
28+
<div className={'mt-6'}>
29+
<button
30+
onClick={() => setVisible(true)}
31+
className={'btn btn-green btn-secondary btn-sm'}
32+
>
33+
Begin Setup
34+
</button>
35+
</div>
36+
</div>
37+
;
38+
};

0 commit comments

Comments
 (0)