Update totp disable modal; require password for enable operation
This commit is contained in:
parent
92926ca193
commit
2d836156d2
10 changed files with 182 additions and 121 deletions
|
@ -8,7 +8,6 @@ use Illuminate\Http\Response;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Pterodactyl\Facades\Activity;
|
use Pterodactyl\Facades\Activity;
|
||||||
use Illuminate\Contracts\Validation\Factory;
|
use Illuminate\Contracts\Validation\Factory;
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
use Pterodactyl\Services\Users\TwoFactorSetupService;
|
use Pterodactyl\Services\Users\TwoFactorSetupService;
|
||||||
use Pterodactyl\Services\Users\ToggleTwoFactorService;
|
use Pterodactyl\Services\Users\ToggleTwoFactorService;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
@ -73,22 +72,20 @@ class TwoFactorController extends ClientApiController
|
||||||
*
|
*
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
|
|
||||||
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
|
|
||||||
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
|
|
||||||
*/
|
*/
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$validator = $this->validation->make($request->all(), [
|
$validator = $this->validation->make($request->all(), [
|
||||||
'code' => 'required|string',
|
'code' => ['required', 'string', 'size:6'],
|
||||||
|
'password' => ['required', 'string'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
$data = $validator->validate();
|
||||||
throw new ValidationException($validator);
|
if (!password_verify($data['password'], $request->user()->password)) {
|
||||||
|
throw new BadRequestHttpException('The password provided was not valid.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$tokens = $this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true);
|
$tokens = $this->toggleTwoFactorService->handle($request->user(), $data['code'], true);
|
||||||
|
|
||||||
Activity::event('user:two-factor.create')->log();
|
Activity::event('user:two-factor.create')->log();
|
||||||
|
|
||||||
|
@ -105,6 +102,7 @@ class TwoFactorController extends ClientApiController
|
||||||
* is valid.
|
* is valid.
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function delete(Request $request)
|
public function delete(Request $request)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
export default async (code: string): Promise<string[]> => {
|
export default async (code: string, password: string): Promise<string[]> => {
|
||||||
const { data } = await http.post('/api/client/account/two-factor', { code });
|
const { data } = await http.post('/api/client/account/two-factor', { code, password });
|
||||||
|
|
||||||
return data.attributes.tokens;
|
return data.attributes.tokens;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,24 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useStoreState } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { Button } from '@/components/elements/button/index';
|
import { Button } from '@/components/elements/button/index';
|
||||||
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
|
import SetupTOTPDialog from '@/components/dashboard/forms/SetupTOTPDialog';
|
||||||
import SetupTOTPModal from '@/components/dashboard/forms/SetupTOTPModal';
|
|
||||||
import RecoveryTokensDialog from '@/components/dashboard/forms/RecoveryTokensDialog';
|
import RecoveryTokensDialog from '@/components/dashboard/forms/RecoveryTokensDialog';
|
||||||
|
import DisableTOTPDialog from '@/components/dashboard/forms/DisableTOTPDialog';
|
||||||
|
import { useFlashKey } from '@/plugins/useFlash';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [tokens, setTokens] = useState<string[]>([]);
|
const [tokens, setTokens] = useState<string[]>([]);
|
||||||
const [visible, setVisible] = useState<'enable' | 'disable' | null>(null);
|
const [visible, setVisible] = useState<'enable' | 'disable' | null>(null);
|
||||||
const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp);
|
const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp);
|
||||||
|
const { clearAndAddHttpError } = useFlashKey('account:two-step');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearAndAddHttpError();
|
||||||
|
};
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
const onTokens = (tokens: string[]) => {
|
const onTokens = (tokens: string[]) => {
|
||||||
setTokens(tokens);
|
setTokens(tokens);
|
||||||
|
@ -19,9 +27,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SetupTOTPModal open={visible === 'enable'} onClose={() => setVisible(null)} onTokens={onTokens} />
|
<SetupTOTPDialog open={visible === 'enable'} onClose={() => setVisible(null)} onTokens={onTokens} />
|
||||||
<RecoveryTokensDialog tokens={tokens} open={tokens.length > 0} onClose={() => setTokens([])} />
|
<RecoveryTokensDialog tokens={tokens} open={tokens.length > 0} onClose={() => setTokens([])} />
|
||||||
<DisableTwoFactorModal visible={visible === 'disable'} onModalDismissed={() => setVisible(null)} />
|
<DisableTOTPDialog open={visible === 'disable'} onClose={() => setVisible(null)} />
|
||||||
<p css={tw`text-sm`}>
|
<p css={tw`text-sm`}>
|
||||||
{isEnabled
|
{isEnabled
|
||||||
? 'Two-step verification is currently enabled on your account.'
|
? 'Two-step verification is currently enabled on your account.'
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import asDialog from '@/hoc/asDialog';
|
||||||
|
import { Dialog, DialogWrapperContext } from '@/components/elements/dialog';
|
||||||
|
import { Button } from '@/components/elements/button/index';
|
||||||
|
import { Input } from '@/components/elements/inputs';
|
||||||
|
import Tooltip from '@/components/elements/tooltip/Tooltip';
|
||||||
|
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
|
||||||
|
import { useFlashKey } from '@/plugins/useFlash';
|
||||||
|
import { useStoreActions } from '@/state/hooks';
|
||||||
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
|
||||||
|
const DisableTOTPDialog = () => {
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const { clearAndAddHttpError } = useFlashKey('account:two-step');
|
||||||
|
const { close, setProps } = useContext(DialogWrapperContext);
|
||||||
|
const updateUserData = useStoreActions((actions) => actions.user.updateUserData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProps((state) => ({ ...state, preventExternalClose: submitting }));
|
||||||
|
}, [submitting]);
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (submitting) return;
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
clearAndAddHttpError();
|
||||||
|
disableAccountTwoFactor(password)
|
||||||
|
.then(() => {
|
||||||
|
updateUserData({ useTotp: false });
|
||||||
|
close();
|
||||||
|
})
|
||||||
|
.catch(clearAndAddHttpError)
|
||||||
|
.then(() => setSubmitting(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id={'disable-totp-form'} className={'mt-6'} onSubmit={submit}>
|
||||||
|
<FlashMessageRender byKey={'account:two-step'} className={'-mt-2 mb-6'} />
|
||||||
|
<label className={'block pb-1'} htmlFor={'totp-password'}>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<Input.Text
|
||||||
|
id={'totp-password'}
|
||||||
|
type={'password'}
|
||||||
|
variant={Input.Text.Variants.Loose}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button.Text onClick={close}>Cancel</Button.Text>
|
||||||
|
<Tooltip
|
||||||
|
delay={100}
|
||||||
|
disabled={password.length > 0}
|
||||||
|
content={'You must enter your account password to continue.'}
|
||||||
|
>
|
||||||
|
<Button.Danger type={'submit'} form={'disable-totp-form'} disabled={submitting || !password.length}>
|
||||||
|
Disable
|
||||||
|
</Button.Danger>
|
||||||
|
</Tooltip>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default asDialog({
|
||||||
|
title: 'Disable Two-Step Verification',
|
||||||
|
description: 'Disabling two-step verification will make your account less secure.',
|
||||||
|
})(DisableTOTPDialog);
|
|
@ -1,73 +0,0 @@
|
||||||
import React, { useContext } from 'react';
|
|
||||||
import { Form, Formik, FormikHelpers } from 'formik';
|
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
|
||||||
import Field from '@/components/elements/Field';
|
|
||||||
import { object, string } from 'yup';
|
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
|
|
||||||
import tw from 'twin.macro';
|
|
||||||
import Button from '@/components/elements/Button';
|
|
||||||
import asModal from '@/hoc/asModal';
|
|
||||||
import ModalContext from '@/context/ModalContext';
|
|
||||||
|
|
||||||
interface Values {
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisableTwoFactorModal = () => {
|
|
||||||
const { dismiss, setPropOverrides } = useContext(ModalContext);
|
|
||||||
const { clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
|
||||||
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
|
|
||||||
|
|
||||||
const submit = ({ password }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
|
||||||
setPropOverrides({ showSpinnerOverlay: true, dismissable: false });
|
|
||||||
disableAccountTwoFactor(password)
|
|
||||||
.then(() => {
|
|
||||||
updateUserData({ useTotp: false });
|
|
||||||
dismiss();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
clearAndAddHttpError({ error, key: 'account:two-factor' });
|
|
||||||
setSubmitting(false);
|
|
||||||
setPropOverrides(null);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
onSubmit={submit}
|
|
||||||
initialValues={{
|
|
||||||
password: '',
|
|
||||||
}}
|
|
||||||
validationSchema={object().shape({
|
|
||||||
password: string().required('You must provide your current password in order to continue.'),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{({ isValid }) => (
|
|
||||||
<Form className={'mb-0'}>
|
|
||||||
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'} />
|
|
||||||
<Field
|
|
||||||
id={'password'}
|
|
||||||
name={'password'}
|
|
||||||
type={'password'}
|
|
||||||
label={'Current Password'}
|
|
||||||
description={
|
|
||||||
'In order to disable two-factor authentication you will need to provide your account password.'
|
|
||||||
}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<div css={tw`mt-6 text-right`}>
|
|
||||||
<Button color={'red'} disabled={!isValid}>
|
|
||||||
Disable Two-Factor
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default asModal()(DisableTwoFactorModal);
|
|
|
@ -22,11 +22,12 @@ interface Props {
|
||||||
const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
const [token, setToken] = useState<TwoFactorTokenData | null>(null);
|
const [token, setToken] = useState<TwoFactorTokenData | null>(null);
|
||||||
const { clearAndAddHttpError } = useFlashKey('account:two-step');
|
const { clearAndAddHttpError } = useFlashKey('account:two-step');
|
||||||
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
|
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
|
||||||
|
|
||||||
const { close } = useContext(DialogWrapperContext);
|
const { close, setProps } = useContext(DialogWrapperContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getTwoFactorTokenData()
|
getTwoFactorTokenData()
|
||||||
|
@ -34,13 +35,19 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||||
.catch((error) => clearAndAddHttpError(error));
|
.catch((error) => clearAndAddHttpError(error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const submit = () => {
|
useEffect(() => {
|
||||||
|
setProps((state) => ({ ...state, preventExternalClose: submitting }));
|
||||||
|
}, [submitting]);
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
if (submitting) return;
|
if (submitting) return;
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
clearAndAddHttpError();
|
clearAndAddHttpError();
|
||||||
|
enableAccountTwoFactor(value, password)
|
||||||
enableAccountTwoFactor(value)
|
|
||||||
.then((tokens) => {
|
.then((tokens) => {
|
||||||
updateUserData({ useTotp: true });
|
updateUserData({ useTotp: true });
|
||||||
onTokens(tokens);
|
onTokens(tokens);
|
||||||
|
@ -52,7 +59,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<form id={'enable-totp-form'} onSubmit={submit}>
|
||||||
<FlashMessageRender byKey={'account:two-step'} className={'mt-4'} />
|
<FlashMessageRender byKey={'account:two-step'} className={'mt-4'} />
|
||||||
<div
|
<div
|
||||||
className={'flex items-center justify-center w-56 h-56 p-2 bg-gray-800 rounded-lg shadow mx-auto mt-6'}
|
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) => {
|
||||||
{token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'}
|
{token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'}
|
||||||
</p>
|
</p>
|
||||||
</CopyOnClick>
|
</CopyOnClick>
|
||||||
<div className={'mt-6'}>
|
<p id={'totp-code-description'} className={'mt-6'}>
|
||||||
<p>
|
Scan the QR code above using the two-step authentication app of your choice. Then, enter the 6-digit
|
||||||
Scan the QR code above using the two-step authentication app of your choice. Then, enter the 6-digit
|
code generated into the field below.
|
||||||
code generated into the field below.
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Input.Text
|
<Input.Text
|
||||||
|
aria-labelledby={'totp-code-description'}
|
||||||
variant={Input.Text.Variants.Loose}
|
variant={Input.Text.Variants.Loose}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.currentTarget.value)}
|
onChange={(e) => setValue(e.currentTarget.value)}
|
||||||
className={'mt-4'}
|
className={'mt-3'}
|
||||||
placeholder={'000000'}
|
placeholder={'000000'}
|
||||||
type={'text'}
|
type={'text'}
|
||||||
inputMode={'numeric'}
|
inputMode={'numeric'}
|
||||||
autoComplete={'one-time-code'}
|
autoComplete={'one-time-code'}
|
||||||
pattern={'\\d{6}'}
|
pattern={'\\d{6}'}
|
||||||
/>
|
/>
|
||||||
|
<label htmlFor={'totp-password'} className={'block mt-3'}>
|
||||||
|
Account Password
|
||||||
|
</label>
|
||||||
|
<Input.Text
|
||||||
|
variant={Input.Text.Variants.Loose}
|
||||||
|
className={'mt-1'}
|
||||||
|
type={'password'}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
<Dialog.Footer>
|
<Dialog.Footer>
|
||||||
<Button.Text onClick={close}>Cancel</Button.Text>
|
<Button.Text onClick={close}>Cancel</Button.Text>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
disabled={value.length === 6}
|
disabled={password.length > 0 && value.length === 6}
|
||||||
content={!token ? 'Waiting for QR code to load...' : 'You must enter the 6-digit code to continue.'}
|
content={
|
||||||
|
!token
|
||||||
|
? 'Waiting for QR code to load...'
|
||||||
|
: 'You must enter the 6-digit code and your password to continue.'
|
||||||
|
}
|
||||||
delay={100}
|
delay={100}
|
||||||
>
|
>
|
||||||
<Button disabled={!token || value.length !== 6} onClick={submit}>
|
<Button
|
||||||
|
disabled={!token || value.length !== 6 || !password.length}
|
||||||
|
type={'submit'}
|
||||||
|
form={'enable-totp-form'}
|
||||||
|
>
|
||||||
Enable
|
Enable
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Field from '@/components/elements/Field';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import { Button } from '@/components/elements/button/index';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -66,9 +66,7 @@ export default () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
<Button disabled={isSubmitting || !isValid}>Update Email</Button>
|
||||||
Update Email
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import updateAccountPassword from '@/api/account/updateAccountPassword';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import { Button } from '@/components/elements/button/index';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
current: string;
|
current: string;
|
||||||
|
@ -91,9 +91,7 @@ export default () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
<Button disabled={isSubmitting || !isValid}>Update Password</Button>
|
||||||
Update Password
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -27,7 +27,7 @@ export interface RenderDialogProps extends DialogProps {
|
||||||
export type WrapperProps = Omit<RenderDialogProps, 'children' | 'open' | 'onClose'>;
|
export type WrapperProps = Omit<RenderDialogProps, 'children' | 'open' | 'onClose'>;
|
||||||
export interface DialogWrapperContextType {
|
export interface DialogWrapperContextType {
|
||||||
props: Readonly<WrapperProps>;
|
props: Readonly<WrapperProps>;
|
||||||
setProps: Callback<Partial<WrapperProps>>;
|
setProps: React.Dispatch<React.SetStateAction<WrapperProps>>;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,13 +59,13 @@ class TwoFactorControllerTest extends ClientApiIntegrationTestCase
|
||||||
/** @var \Pterodactyl\Models\User $user */
|
/** @var \Pterodactyl\Models\User $user */
|
||||||
$user = User::factory()->create(['use_totp' => false]);
|
$user = User::factory()->create(['use_totp' => false]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [
|
$this->actingAs($user)
|
||||||
'code' => '',
|
->postJson('/api/client/account/two-factor', ['code' => ''])
|
||||||
]);
|
->assertUnprocessable()
|
||||||
|
->assertJsonPath('errors.0.meta.rule', 'required')
|
||||||
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
|
->assertJsonPath('errors.0.meta.source_field', 'code')
|
||||||
$response->assertJsonPath('errors.0.code', 'ValidationException');
|
->assertJsonPath('errors.1.meta.rule', 'required')
|
||||||
$response->assertJsonPath('errors.0.meta.rule', 'required');
|
->assertJsonPath('errors.1.meta.source_field', 'password');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,6 +90,7 @@ class TwoFactorControllerTest extends ClientApiIntegrationTestCase
|
||||||
|
|
||||||
$response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [
|
$response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [
|
||||||
'code' => $token,
|
'code' => $token,
|
||||||
|
'password' => 'password',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
|
@ -168,4 +169,39 @@ class TwoFactorControllerTest extends ClientApiIntegrationTestCase
|
||||||
|
|
||||||
$response->assertStatus(Response::HTTP_NO_CONTENT);
|
$response->assertStatus(Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a valid account password is required when enabling two-factor.
|
||||||
|
*/
|
||||||
|
public function testEnablingTwoFactorRequiresValidPassword()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['use_totp' => false]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->postJson('/api/client/account/two-factor', [
|
||||||
|
'code' => '123456',
|
||||||
|
'password' => 'foo',
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_BAD_REQUEST)
|
||||||
|
->assertJsonPath('errors.0.detail', 'The password provided was not valid.');
|
||||||
|
|
||||||
|
$this->assertFalse($user->refresh()->use_totp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a valid account password is required when disabling two-factor.
|
||||||
|
*/
|
||||||
|
public function testDisablingTwoFactorRequiresValidPassword()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['use_totp' => true]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->deleteJson('/api/client/account/two-factor', [
|
||||||
|
'password' => 'foo',
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_BAD_REQUEST)
|
||||||
|
->assertJsonPath('errors.0.detail', 'The password provided was not valid.');
|
||||||
|
|
||||||
|
$this->assertTrue($user->refresh()->use_totp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue