Skip to content

Commit 2d83615

Browse files
committed
Update totp disable modal; require password for enable operation
1 parent 92926ca commit 2d83615

File tree

10 files changed

+182
-121
lines changed

10 files changed

+182
-121
lines changed

app/Http/Controllers/Api/Client/TwoFactorController.php

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Illuminate\Http\JsonResponse;
99
use Pterodactyl\Facades\Activity;
1010
use Illuminate\Contracts\Validation\Factory;
11-
use Illuminate\Validation\ValidationException;
1211
use Pterodactyl\Services\Users\TwoFactorSetupService;
1312
use Pterodactyl\Services\Users\ToggleTwoFactorService;
1413
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@@ -73,22 +72,20 @@ public function index(Request $request)
7372
*
7473
* @throws \Throwable
7574
* @throws \Illuminate\Validation\ValidationException
76-
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
77-
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
78-
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
79-
* @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
8075
*/
8176
public function store(Request $request)
8277
{
8378
$validator = $this->validation->make($request->all(), [
84-
'code' => 'required|string',
79+
'code' => ['required', 'string', 'size:6'],
80+
'password' => ['required', 'string'],
8581
]);
8682

87-
if ($validator->fails()) {
88-
throw new ValidationException($validator);
83+
$data = $validator->validate();
84+
if (!password_verify($data['password'], $request->user()->password)) {
85+
throw new BadRequestHttpException('The password provided was not valid.');
8986
}
9087

91-
$tokens = $this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true);
88+
$tokens = $this->toggleTwoFactorService->handle($request->user(), $data['code'], true);
9289

9390
Activity::event('user:two-factor.create')->log();
9491

@@ -105,6 +102,7 @@ public function store(Request $request)
105102
* is valid.
106103
*
107104
* @return \Illuminate\Http\JsonResponse
105+
* @throws \Throwable
108106
*/
109107
public function delete(Request $request)
110108
{
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import http from '@/api/http';
22

3-
export default async (code: string): Promise<string[]> => {
4-
const { data } = await http.post('/api/client/account/two-factor', { code });
3+
export default async (code: string, password: string): Promise<string[]> => {
4+
const { data } = await http.post('/api/client/account/two-factor', { code, password });
55

66
return data.attributes.tokens;
77
};

resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1-
import React, { useState } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { useStoreState } from 'easy-peasy';
33
import { ApplicationStore } from '@/state';
44
import tw from 'twin.macro';
55
import { Button } from '@/components/elements/button/index';
6-
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
7-
import SetupTOTPModal from '@/components/dashboard/forms/SetupTOTPModal';
6+
import SetupTOTPDialog from '@/components/dashboard/forms/SetupTOTPDialog';
87
import RecoveryTokensDialog from '@/components/dashboard/forms/RecoveryTokensDialog';
8+
import DisableTOTPDialog from '@/components/dashboard/forms/DisableTOTPDialog';
9+
import { useFlashKey } from '@/plugins/useFlash';
910

1011
export default () => {
1112
const [tokens, setTokens] = useState<string[]>([]);
1213
const [visible, setVisible] = useState<'enable' | 'disable' | null>(null);
1314
const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp);
15+
const { clearAndAddHttpError } = useFlashKey('account:two-step');
16+
17+
useEffect(() => {
18+
return () => {
19+
clearAndAddHttpError();
20+
};
21+
}, [visible]);
1422

1523
const onTokens = (tokens: string[]) => {
1624
setTokens(tokens);
@@ -19,9 +27,9 @@ export default () => {
1927

2028
return (
2129
<div>
22-
<SetupTOTPModal open={visible === 'enable'} onClose={() => setVisible(null)} onTokens={onTokens} />
30+
<SetupTOTPDialog open={visible === 'enable'} onClose={() => setVisible(null)} onTokens={onTokens} />
2331
<RecoveryTokensDialog tokens={tokens} open={tokens.length > 0} onClose={() => setTokens([])} />
24-
<DisableTwoFactorModal visible={visible === 'disable'} onModalDismissed={() => setVisible(null)} />
32+
<DisableTOTPDialog open={visible === 'disable'} onClose={() => setVisible(null)} />
2533
<p css={tw`text-sm`}>
2634
{isEnabled
2735
? 'Two-step verification is currently enabled on your account.'
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { useContext, useEffect, useState } from 'react';
2+
import asDialog from '@/hoc/asDialog';
3+
import { Dialog, DialogWrapperContext } from '@/components/elements/dialog';
4+
import { Button } from '@/components/elements/button/index';
5+
import { Input } from '@/components/elements/inputs';
6+
import Tooltip from '@/components/elements/tooltip/Tooltip';
7+
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
8+
import { useFlashKey } from '@/plugins/useFlash';
9+
import { useStoreActions } from '@/state/hooks';
10+
import FlashMessageRender from '@/components/FlashMessageRender';
11+
12+
const DisableTOTPDialog = () => {
13+
const [submitting, setSubmitting] = useState(false);
14+
const [password, setPassword] = useState('');
15+
const { clearAndAddHttpError } = useFlashKey('account:two-step');
16+
const { close, setProps } = useContext(DialogWrapperContext);
17+
const updateUserData = useStoreActions((actions) => actions.user.updateUserData);
18+
19+
useEffect(() => {
20+
setProps((state) => ({ ...state, preventExternalClose: submitting }));
21+
}, [submitting]);
22+
23+
const submit = (e: React.FormEvent<HTMLFormElement>) => {
24+
e.preventDefault();
25+
e.stopPropagation();
26+
27+
if (submitting) return;
28+
29+
setSubmitting(true);
30+
clearAndAddHttpError();
31+
disableAccountTwoFactor(password)
32+
.then(() => {
33+
updateUserData({ useTotp: false });
34+
close();
35+
})
36+
.catch(clearAndAddHttpError)
37+
.then(() => setSubmitting(false));
38+
};
39+
40+
return (
41+
<form id={'disable-totp-form'} className={'mt-6'} onSubmit={submit}>
42+
<FlashMessageRender byKey={'account:two-step'} className={'-mt-2 mb-6'} />
43+
<label className={'block pb-1'} htmlFor={'totp-password'}>
44+
Password
45+
</label>
46+
<Input.Text
47+
id={'totp-password'}
48+
type={'password'}
49+
variant={Input.Text.Variants.Loose}
50+
value={password}
51+
onChange={(e) => setPassword(e.currentTarget.value)}
52+
/>
53+
<Dialog.Footer>
54+
<Button.Text onClick={close}>Cancel</Button.Text>
55+
<Tooltip
56+
delay={100}
57+
disabled={password.length > 0}
58+
content={'You must enter your account password to continue.'}
59+
>
60+
<Button.Danger type={'submit'} form={'disable-totp-form'} disabled={submitting || !password.length}>
61+
Disable
62+
</Button.Danger>
63+
</Tooltip>
64+
</Dialog.Footer>
65+
</form>
66+
);
67+
};
68+
69+
export default asDialog({
70+
title: 'Disable Two-Step Verification',
71+
description: 'Disabling two-step verification will make your account less secure.',
72+
})(DisableTOTPDialog);

resources/scripts/components/dashboard/forms/DisableTwoFactorModal.tsx

Lines changed: 0 additions & 73 deletions
This file was deleted.

resources/scripts/components/dashboard/forms/SetupTOTPModal.tsx renamed to resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,32 @@ interface Props {
2222
const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
2323
const [submitting, setSubmitting] = useState(false);
2424
const [value, setValue] = useState('');
25+
const [password, setPassword] = useState('');
2526
const [token, setToken] = useState<TwoFactorTokenData | null>(null);
2627
const { clearAndAddHttpError } = useFlashKey('account:two-step');
2728
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
2829

29-
const { close } = useContext(DialogWrapperContext);
30+
const { close, setProps } = useContext(DialogWrapperContext);
3031

3132
useEffect(() => {
3233
getTwoFactorTokenData()
3334
.then(setToken)
3435
.catch((error) => clearAndAddHttpError(error));
3536
}, []);
3637

37-
const submit = () => {
38+
useEffect(() => {
39+
setProps((state) => ({ ...state, preventExternalClose: submitting }));
40+
}, [submitting]);
41+
42+
const submit = (e: React.FormEvent<HTMLFormElement>) => {
43+
e.preventDefault();
44+
e.stopPropagation();
45+
3846
if (submitting) return;
3947

4048
setSubmitting(true);
4149
clearAndAddHttpError();
42-
43-
enableAccountTwoFactor(value)
50+
enableAccountTwoFactor(value, password)
4451
.then((tokens) => {
4552
updateUserData({ useTotp: true });
4653
onTokens(tokens);
@@ -52,7 +59,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
5259
};
5360

5461
return (
55-
<>
62+
<form id={'enable-totp-form'} onSubmit={submit}>
5663
<FlashMessageRender byKey={'account:two-step'} className={'mt-4'} />
5764
<div
5865
className={'flex items-center justify-center w-56 h-56 p-2 bg-gray-800 rounded-lg shadow mx-auto mt-6'}
@@ -68,36 +75,53 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
6875
{token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'}
6976
</p>
7077
</CopyOnClick>
71-
<div className={'mt-6'}>
72-
<p>
73-
Scan the QR code above using the two-step authentication app of your choice. Then, enter the 6-digit
74-
code generated into the field below.
75-
</p>
76-
</div>
78+
<p id={'totp-code-description'} className={'mt-6'}>
79+
Scan the QR code above using the two-step authentication app of your choice. Then, enter the 6-digit
80+
code generated into the field below.
81+
</p>
7782
<Input.Text
83+
aria-labelledby={'totp-code-description'}
7884
variant={Input.Text.Variants.Loose}
7985
value={value}
8086
onChange={(e) => setValue(e.currentTarget.value)}
81-
className={'mt-4'}
87+
className={'mt-3'}
8288
placeholder={'000000'}
8389
type={'text'}
8490
inputMode={'numeric'}
8591
autoComplete={'one-time-code'}
8692
pattern={'\\d{6}'}
8793
/>
94+
<label htmlFor={'totp-password'} className={'block mt-3'}>
95+
Account Password
96+
</label>
97+
<Input.Text
98+
variant={Input.Text.Variants.Loose}
99+
className={'mt-1'}
100+
type={'password'}
101+
value={password}
102+
onChange={(e) => setPassword(e.currentTarget.value)}
103+
/>
88104
<Dialog.Footer>
89105
<Button.Text onClick={close}>Cancel</Button.Text>
90106
<Tooltip
91-
disabled={value.length === 6}
92-
content={!token ? 'Waiting for QR code to load...' : 'You must enter the 6-digit code to continue.'}
107+
disabled={password.length > 0 && value.length === 6}
108+
content={
109+
!token
110+
? 'Waiting for QR code to load...'
111+
: 'You must enter the 6-digit code and your password to continue.'
112+
}
93113
delay={100}
94114
>
95-
<Button disabled={!token || value.length !== 6} onClick={submit}>
115+
<Button
116+
disabled={!token || value.length !== 6 || !password.length}
117+
type={'submit'}
118+
form={'enable-totp-form'}
119+
>
96120
Enable
97121
</Button>
98122
</Tooltip>
99123
</Dialog.Footer>
100-
</>
124+
</form>
101125
);
102126
};
103127

resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Field from '@/components/elements/Field';
77
import { httpErrorToHuman } from '@/api/http';
88
import { ApplicationStore } from '@/state';
99
import tw from 'twin.macro';
10-
import Button from '@/components/elements/Button';
10+
import { Button } from '@/components/elements/button/index';
1111

1212
interface Values {
1313
email: string;
@@ -66,9 +66,7 @@ export default () => {
6666
/>
6767
</div>
6868
<div css={tw`mt-6`}>
69-
<Button size={'small'} disabled={isSubmitting || !isValid}>
70-
Update Email
71-
</Button>
69+
<Button disabled={isSubmitting || !isValid}>Update Email</Button>
7270
</div>
7371
</Form>
7472
</React.Fragment>

resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import updateAccountPassword from '@/api/account/updateAccountPassword';
88
import { httpErrorToHuman } from '@/api/http';
99
import { ApplicationStore } from '@/state';
1010
import tw from 'twin.macro';
11-
import Button from '@/components/elements/Button';
11+
import { Button } from '@/components/elements/button/index';
1212

1313
interface Values {
1414
current: string;
@@ -91,9 +91,7 @@ export default () => {
9191
/>
9292
</div>
9393
<div css={tw`mt-6`}>
94-
<Button size={'small'} disabled={isSubmitting || !isValid}>
95-
Update Password
96-
</Button>
94+
<Button disabled={isSubmitting || !isValid}>Update Password</Button>
9795
</div>
9896
</Form>
9997
</React.Fragment>

0 commit comments

Comments
 (0)