feat: create security container and frontend for 2fa settings
This commit is contained in:
parent
866bee4491
commit
904d2c5adc
8 changed files with 495 additions and 1 deletions
|
@ -56,6 +56,7 @@
|
|||
"node-cron": "^3.0.1",
|
||||
"node-fetch-commonjs": "^3.2.4",
|
||||
"pg": "^8.10.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.43.7",
|
||||
|
|
|
@ -82,6 +82,9 @@ dependencies:
|
|||
pg:
|
||||
specifier: ^8.10.0
|
||||
version: 8.10.0
|
||||
qrcode.react:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(react@18.2.0)
|
||||
react:
|
||||
specifier: 18.2.0
|
||||
version: 18.2.0
|
||||
|
@ -7143,6 +7146,14 @@ packages:
|
|||
resolution: {integrity: sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==}
|
||||
dev: true
|
||||
|
||||
/qrcode.react@3.1.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/qs@6.11.0:
|
||||
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
|
|
@ -13,16 +13,20 @@ interface IProps {
|
|||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||
disabled?: boolean;
|
||||
value?: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, IProps>(({ onChange, onBlur, name, label, placeholder, error, type = 'text', className, value, isInvalid, disabled }, ref) => (
|
||||
export const Input = React.forwardRef<HTMLInputElement, IProps>(({ onChange, onBlur, name, label, placeholder, error, type = 'text', className, value, isInvalid, disabled, readOnly }, ref) => (
|
||||
<div className={clsx(className)}>
|
||||
{label && (
|
||||
<label htmlFor={name} className="form-label">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
|
||||
<input
|
||||
aria-label={name}
|
||||
role="textbox"
|
||||
disabled={disabled}
|
||||
name={name}
|
||||
id={name}
|
||||
|
@ -33,6 +37,7 @@ export const Input = React.forwardRef<HTMLInputElement, IProps>(({ onChange, onB
|
|||
ref={ref}
|
||||
className={clsx('form-control', { 'is-invalid is-invalid-lite': error || isInvalid })}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{error && <div className="invalid-feedback">{error}</div>}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,307 @@
|
|||
import React from 'react';
|
||||
import { server } from '@/client/mocks/server';
|
||||
import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
|
||||
import { useToastStore } from '@/client/state/toastStore';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils';
|
||||
import { SecurityContainer } from './SecurityContainer';
|
||||
|
||||
describe('<SecurityContainer />', () => {
|
||||
it('should render', () => {
|
||||
render(<SecurityContainer />);
|
||||
});
|
||||
|
||||
it('should prompt for password when enabling 2FA', async () => {
|
||||
// arrange
|
||||
render(<SecurityContainer />);
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should prompt for password when disabling 2FA', async () => {
|
||||
// arrange
|
||||
server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, id: 12, username: 'test' } }));
|
||||
render(<SecurityContainer />);
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show show error toast if password is incorrect while enabling 2FA', async () => {
|
||||
// arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: false, id: 12, username: 'test' } }));
|
||||
server.use(getTRPCMockError({ path: ['auth', 'getTotpUri'], type: 'mutation', message: 'Invalid password' }));
|
||||
render(<SecurityContainer />);
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByRole('textbox', { name: 'password' });
|
||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||
const submitButton = screen.getByRole('button', { name: /Enable 2FA/i });
|
||||
submitButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]?.status).toEqual('error');
|
||||
expect(result.current.toasts[0]?.title).toEqual('Error');
|
||||
expect(result.current.toasts[0]?.description).toEqual('Invalid password');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show show error toast if password is incorrect while disabling 2FA', async () => {
|
||||
// arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, id: 12, username: 'test' } }));
|
||||
server.use(getTRPCMockError({ path: ['auth', 'disableTotp'], type: 'mutation', message: 'Invalid password' }));
|
||||
render(<SecurityContainer />);
|
||||
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByRole('textbox', { name: 'password' });
|
||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||
const submitButton = screen.getByRole('button', { name: /Disable 2FA/i });
|
||||
submitButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]?.status).toEqual('error');
|
||||
expect(result.current.toasts[0]?.title).toEqual('Error');
|
||||
expect(result.current.toasts[0]?.description).toEqual('Invalid password');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show success toast if password is correct while disabling 2FA', async () => {
|
||||
// arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, id: 12, username: 'test' } }));
|
||||
server.use(getTRPCMock({ path: ['auth', 'disableTotp'], type: 'mutation', response: true }));
|
||||
|
||||
render(<SecurityContainer />);
|
||||
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByRole('textbox', { name: 'password' });
|
||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||
const submitButton = screen.getByRole('button', { name: /Disable 2FA/i });
|
||||
submitButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]?.status).toEqual('success');
|
||||
expect(result.current.toasts[0]?.title).toEqual('Success');
|
||||
expect(result.current.toasts[0]?.description).toEqual('Two-factor authentication disabled');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show secret key and QR code when enabling 2FA', async () => {
|
||||
// arrange
|
||||
server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } }));
|
||||
render(<SecurityContainer />);
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
const passwordInput = screen.getByRole('textbox', { name: 'password' });
|
||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||
const submitButton = screen.getByRole('button', { name: /Enable 2FA/i });
|
||||
submitButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Scan this QR code with your authenticator app.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: 'secret key' })).toHaveValue('test');
|
||||
expect(screen.getByRole('button', { name: 'Enable 2FA' })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast if submitted totp code is invalid', async () => {
|
||||
// arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } }));
|
||||
server.use(getTRPCMockError({ path: ['auth', 'setupTotp'], type: 'mutation', message: 'Invalid code' }));
|
||||
|
||||
render(<SecurityContainer />);
|
||||
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
const passwordInput = screen.getByRole('textbox', { name: 'password' });
|
||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||
const submitButton = screen.getByRole('button', { name: /Enable 2FA/i });
|
||||
submitButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Scan this QR code with your authenticator app.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const inputEls = screen.getAllByRole('textbox', { name: /digit-/ });
|
||||
|
||||
inputEls.forEach((inputEl) => {
|
||||
fireEvent.change(inputEl, { target: { value: '1' } });
|
||||
});
|
||||
|
||||
const enable2FAButton = screen.getByRole('button', { name: 'Enable 2FA' });
|
||||
enable2FAButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]?.status).toEqual('error');
|
||||
expect(result.current.toasts[0]?.title).toEqual('Error');
|
||||
expect(result.current.toasts[0]?.description).toEqual('Invalid code');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show success toast if submitted totp code is valid', async () => {
|
||||
// arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } }));
|
||||
server.use(getTRPCMock({ path: ['auth', 'setupTotp'], type: 'mutation', response: true }));
|
||||
render(<SecurityContainer />);
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
const passwordInput = screen.getByRole('textbox', { name: 'password' });
|
||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||
const submitButton = screen.getByRole('button', { name: /Enable 2FA/i });
|
||||
submitButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Scan this QR code with your authenticator app.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const inputEls = screen.getAllByRole('textbox', { name: /digit-/ });
|
||||
|
||||
inputEls.forEach((inputEl) => {
|
||||
fireEvent.change(inputEl, { target: { value: '1' } });
|
||||
});
|
||||
|
||||
const enable2FAButton = screen.getByRole('button', { name: 'Enable 2FA' });
|
||||
enable2FAButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]?.status).toEqual('success');
|
||||
expect(result.current.toasts[0]?.title).toEqual('Success');
|
||||
expect(result.current.toasts[0]?.description).toEqual('Two-factor authentication enabled');
|
||||
});
|
||||
});
|
||||
|
||||
it('can close the setup modal by clicking on the esc key', async () => {
|
||||
// arrange
|
||||
render(<SecurityContainer />);
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Password needed')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('can close the disable modal by clicking on the esc key', async () => {
|
||||
// arrange
|
||||
server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, username: '', id: 1 } }));
|
||||
render(<SecurityContainer />);
|
||||
const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i });
|
||||
await waitFor(() => {
|
||||
expect(twoFactorAuthButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// act
|
||||
twoFactorAuthButton.click();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password needed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Password needed')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,162 @@
|
|||
import { Switch } from '@/components/ui/Switch';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import React from 'react';
|
||||
import { trpc } from '@/utils/trpc';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { useToastStore } from '@/client/state/toastStore';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { OtpInput } from '@/components/ui/OtpInput';
|
||||
import { IconLock } from '@tabler/icons-react';
|
||||
|
||||
export const SecurityContainer = () => {
|
||||
const { addToast } = useToastStore();
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [key, setKey] = React.useState('');
|
||||
const [uri, setUri] = React.useState('');
|
||||
const [totpCode, setTotpCode] = React.useState('');
|
||||
|
||||
// Dialog statuses
|
||||
const [isSetupTotpOpen, setIsSetupTotpOpen] = React.useState(false);
|
||||
const [isDisableTotpOpen, setIsDisableTotpOpen] = React.useState(false);
|
||||
|
||||
const ctx = trpc.useContext();
|
||||
const me = trpc.auth.me.useQuery();
|
||||
|
||||
const getTotpUri = trpc.auth.getTotpUri.useMutation({
|
||||
onMutate: () => {
|
||||
setIsSetupTotpOpen(false);
|
||||
},
|
||||
onError: (e) => {
|
||||
setPassword('');
|
||||
addToast({ title: 'Error', description: e.message, status: 'error' });
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setKey(data.key);
|
||||
setUri(data.uri);
|
||||
},
|
||||
});
|
||||
|
||||
const setupTotp = trpc.auth.setupTotp.useMutation({
|
||||
onMutate: () => {},
|
||||
onError: (e) => {
|
||||
setTotpCode('');
|
||||
addToast({ title: 'Error', description: e.message, status: 'error' });
|
||||
},
|
||||
onSuccess: () => {
|
||||
setTotpCode('');
|
||||
setKey('');
|
||||
setUri('');
|
||||
addToast({ title: 'Success', description: 'Two-factor authentication enabled', status: 'success' });
|
||||
ctx.auth.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const disableTotp = trpc.auth.disableTotp.useMutation({
|
||||
onMutate: () => {
|
||||
setIsDisableTotpOpen(false);
|
||||
},
|
||||
onError: (e) => {
|
||||
setPassword('');
|
||||
addToast({ title: 'Error', description: e.message, status: 'error' });
|
||||
},
|
||||
onSuccess: () => {
|
||||
addToast({ title: 'Success', description: 'Two-factor authentication disabled', status: 'success' });
|
||||
ctx.auth.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const handleTotp = (enabled: boolean) => {
|
||||
if (enabled) setIsSetupTotpOpen(true);
|
||||
else {
|
||||
setIsDisableTotpOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSetupQr = () => {
|
||||
if (!uri || me.data?.totp_enabled) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-muted">Scan this QR code with your authenticator app.</p>
|
||||
<QRCodeSVG value={uri} />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-muted">Or enter this key manually.</p>
|
||||
<Input name="secret key" value={key} readOnly />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-muted">Enter the code from your authenticator app.</p>
|
||||
<OtpInput value={totpCode} valueLength={6} onChange={(e) => setTotpCode(e)} />
|
||||
<Button disabled={totpCode.trim().length < 6} onClick={() => setupTotp.mutate({ totpCode })} className="mt-3 btn-success">
|
||||
Enable 2FA
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card-body">
|
||||
<div className="d-flex">
|
||||
<IconLock className="me-2" />
|
||||
<h2>Two-Factor Authentication</h2>
|
||||
</div>
|
||||
<p className="text-muted">
|
||||
Two-factor authentication (2FA) adds an additional layer of security to your account.
|
||||
<br />
|
||||
When enabled, you will be prompted to enter a code from your authenticator app when you log in.
|
||||
</p>
|
||||
{!key && <Switch disabled={!me.isSuccess} onCheckedChange={handleTotp} checked={me.data?.totp_enabled} label="Enable two-factor authentication" />}
|
||||
{getTotpUri.isLoading && (
|
||||
<div className="progress w-50">
|
||||
<div className="progress-bar progress-bar-indeterminate bg-green" />
|
||||
</div>
|
||||
)}
|
||||
{renderSetupQr()}
|
||||
<Dialog open={isSetupTotpOpen} onOpenChange={(o) => setIsSetupTotpOpen(o)}>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Password needed</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription className="d-flex flex-column">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
getTotpUri.mutate({ password });
|
||||
}}
|
||||
>
|
||||
<p className="text-muted">Your password is required to setup two-factor authentication.</p>
|
||||
<Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
|
||||
<Button loading={getTotpUri.isLoading} type="submit" className="btn-success mt-3">
|
||||
Enable 2FA
|
||||
</Button>
|
||||
</form>
|
||||
</DialogDescription>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isDisableTotpOpen} onOpenChange={(o) => setIsDisableTotpOpen(o)}>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Password needed</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription className="d-flex flex-column">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
disableTotp.mutate({ password });
|
||||
}}
|
||||
>
|
||||
<p className="text-muted">Your password is required to disable two-factor authentication.</p>
|
||||
<Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
|
||||
<Button loading={disableTotp.isLoading} type="submit" className="btn-danger mt-3">
|
||||
Disable 2FA
|
||||
</Button>
|
||||
</form>
|
||||
</DialogDescription>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export { SecurityContainer } from './SecurityContainer';
|
|
@ -4,6 +4,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||
import { Layout } from '../../../../components/Layout';
|
||||
import { GeneralActions } from '../../containers/GeneralActions';
|
||||
import { SettingsContainer } from '../../containers/SettingsContainer';
|
||||
import { SecurityContainer } from '../../containers/SecurityContainer';
|
||||
|
||||
export const SettingsPage: NextPage = () => {
|
||||
return (
|
||||
|
@ -13,6 +14,7 @@ export const SettingsPage: NextPage = () => {
|
|||
<TabsList>
|
||||
<TabsTrigger value="actions">Actions</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="actions">
|
||||
<GeneralActions />
|
||||
|
@ -20,6 +22,9 @@ export const SettingsPage: NextPage = () => {
|
|||
<TabsContent value="settings">
|
||||
<SettingsContainer />
|
||||
</TabsContent>
|
||||
<TabsContent value="security">
|
||||
<SecurityContainer />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Layout>
|
||||
|
|
|
@ -174,6 +174,8 @@ export class AuthServiceClass {
|
|||
totp_enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public disableTotp = async (params: { userId: number; password: string }) => {
|
||||
|
|
Loading…
Reference in a new issue