Skip to content

Commit 9a0ed6b

Browse files
committed
Add ability to disable two factor authentication
1 parent 2a653cd commit 9a0ed6b

File tree

4 files changed

+107
-3
lines changed

4 files changed

+107
-3
lines changed

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Pterodactyl\Http\Controllers\Api\Client;
44

5+
use Carbon\Carbon;
56
use Illuminate\Http\Request;
67
use Illuminate\Http\Response;
78
use Illuminate\Http\JsonResponse;
@@ -100,7 +101,29 @@ public function store(Request $request)
100101
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
101102
}
102103

103-
public function delete()
104+
/**
105+
* Disables two-factor authentication on an account if the password provided
106+
* is valid.
107+
*
108+
* @param \Illuminate\Http\Request $request
109+
* @return \Illuminate\Http\JsonResponse
110+
*/
111+
public function delete(Request $request)
104112
{
113+
if (! password_verify($request->input('password') ?? '', $request->user()->password)) {
114+
throw new BadRequestHttpException(
115+
'The password provided was not valid.'
116+
);
117+
}
118+
119+
/** @var \Pterodactyl\Models\User $user */
120+
$user = $request->user();
121+
122+
$user->update([
123+
'totp_authenticated_at' => Carbon::now(),
124+
'use_totp' => false,
125+
]);
126+
127+
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
105128
}
106129
}
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 (password: string): Promise<void> => {
4+
return new Promise((resolve, reject) => {
5+
http.delete('/api/client/account/two-factor', { params: { password } })
6+
.then(() => resolve())
7+
.catch(reject);
8+
});
9+
};

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@ import React, { useState } from 'react';
22
import { useStoreState } from 'easy-peasy';
33
import { ApplicationStore } from '@/state';
44
import SetupTwoFactorModal from '@/components/dashboard/forms/SetupTwoFactorModal';
5+
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
56

67
export default () => {
78
const user = useStoreState((state: ApplicationStore) => state.user.data!);
8-
const [visible, setVisible] = useState(false);
9+
const [ visible, setVisible ] = useState(false);
910

1011
return user.useTotp ?
1112
<div>
13+
{visible && <DisableTwoFactorModal visible={visible} onDismissed={() => setVisible(false)}/>}
1214
<p className={'text-sm'}>
1315
Two-factor authentication is currently enabled on your account.
1416
</p>
1517
<div className={'mt-6'}>
16-
<button className={'btn btn-red btn-secondary btn-sm'}>
18+
<button
19+
onClick={() => setVisible(true)}
20+
className={'btn btn-red btn-secondary btn-sm'}
21+
>
1722
Disable
1823
</button>
1924
</div>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
import { Form, Formik, FormikActions } from 'formik';
3+
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
4+
import FlashMessageRender from '@/components/FlashMessageRender';
5+
import Field from '@/components/elements/Field';
6+
import { object, string } from 'yup';
7+
import { Actions, useStoreActions } from 'easy-peasy';
8+
import { ApplicationStore } from '@/state';
9+
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
10+
import { httpErrorToHuman } from '@/api/http';
11+
12+
interface Values {
13+
password: string;
14+
}
15+
16+
export default ({ ...props }: RequiredModalProps) => {
17+
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
18+
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
19+
20+
const submit = ({ password }: Values, { setSubmitting }: FormikActions<Values>) => {
21+
clearFlashes('account:two-factor');
22+
disableAccountTwoFactor(password)
23+
.then(() => {
24+
updateUserData({ useTotp: false });
25+
props.onDismissed();
26+
})
27+
.catch(error => {
28+
console.error(error);
29+
30+
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
31+
setSubmitting(false);
32+
});
33+
};
34+
35+
return (
36+
<Formik
37+
onSubmit={submit}
38+
initialValues={{
39+
password: '',
40+
}}
41+
validationSchema={object().shape({
42+
password: string().required('You must provider your current password in order to continue.'),
43+
})}
44+
>
45+
{({ isSubmitting, isValid }) => (
46+
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
47+
<Form className={'mb-0'}>
48+
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
49+
<Field
50+
id={'password'}
51+
name={'password'}
52+
type={'password'}
53+
label={'Current Password'}
54+
description={'In order to disable two-factor authentication you will need to provide your account password.'}
55+
autoFocus={true}
56+
/>
57+
<div className={'mt-6 text-right'}>
58+
<button className={'btn btn-red btn-sm'} disabled={!isValid}>
59+
Disable Two-Factor
60+
</button>
61+
</div>
62+
</Form>
63+
</Modal>
64+
)}
65+
</Formik>
66+
);
67+
};

0 commit comments

Comments
 (0)