pl-fe: migrate 2fa settings to tanstack query
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -1,55 +0,0 @@
|
||||
import { getClient } from '../api';
|
||||
|
||||
import type { PlApiClient } from 'pl-api';
|
||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||
|
||||
const MFA_FETCH_SUCCESS = 'MFA_FETCH_SUCCESS' as const;
|
||||
|
||||
const MFA_CONFIRM_SUCCESS = 'MFA_CONFIRM_SUCCESS' as const;
|
||||
|
||||
const MFA_DISABLE_SUCCESS = 'MFA_DISABLE_SUCCESS' as const;
|
||||
|
||||
const fetchMfa = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).settings.mfa.getMfaSettings().then((data) => {
|
||||
dispatch<MfaAction>({ type: MFA_FETCH_SUCCESS, data });
|
||||
});
|
||||
|
||||
const fetchBackupCodes = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).settings.mfa.getMfaBackupCodes();
|
||||
|
||||
const setupMfa = (method: 'totp') =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).settings.mfa.getMfaSetup(method);
|
||||
|
||||
const confirmMfa = (method: 'totp', code: string, password: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).settings.mfa.confirmMfaSetup(method, code, password).then((data) => {
|
||||
dispatch<MfaAction>({ type: MFA_CONFIRM_SUCCESS, method, code });
|
||||
return data;
|
||||
});
|
||||
|
||||
const disableMfa = (method: 'totp', password: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).settings.mfa.disableMfa(method, password).then((data) => {
|
||||
dispatch<MfaAction>({ type: MFA_DISABLE_SUCCESS, method });
|
||||
return data;
|
||||
});
|
||||
|
||||
type MfaAction =
|
||||
| { type: typeof MFA_FETCH_SUCCESS; data: Awaited<ReturnType<(InstanceType<typeof PlApiClient>)['settings']['mfa']['getMfaSettings']>> }
|
||||
| { type: typeof MFA_CONFIRM_SUCCESS; method: 'totp'; code: string }
|
||||
| { type: typeof MFA_DISABLE_SUCCESS; method: 'totp' }
|
||||
|
||||
export {
|
||||
MFA_FETCH_SUCCESS,
|
||||
MFA_CONFIRM_SUCCESS,
|
||||
MFA_DISABLE_SUCCESS,
|
||||
fetchMfa,
|
||||
fetchBackupCodes,
|
||||
setupMfa,
|
||||
confirmMfa,
|
||||
disableMfa,
|
||||
type MfaAction,
|
||||
};
|
||||
@ -1,12 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { fetchMfa } from 'pl-fe/actions/mfa';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useMfaConfig } from 'pl-fe/queries/settings/use-mfa';
|
||||
|
||||
import DisableOtpForm from './mfa/disable-otp-form';
|
||||
import EnableOtpForm from './mfa/enable-otp-form';
|
||||
@ -18,24 +16,19 @@ const messages = defineMessages({
|
||||
|
||||
const MfaForm: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const [displayOtpForm, setDisplayOtpForm] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchMfa());
|
||||
}, []);
|
||||
const { data: mfa } = useMfaConfig();
|
||||
|
||||
const handleSetupProceedClick = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setDisplayOtpForm(true);
|
||||
};
|
||||
|
||||
const mfa = useAppSelector((state) => state.security.mfa);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
{mfa.settings.totp ? (
|
||||
{mfa?.settings.totp ? (
|
||||
<DisableOtpForm />
|
||||
) : (
|
||||
<Stack space={4}>
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { useState, useCallback } from 'react';
|
||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { disableMfa } from 'pl-fe/actions/mfa';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Form from 'pl-fe/components/ui/form';
|
||||
import FormActions from 'pl-fe/components/ui/form-actions';
|
||||
@ -11,6 +10,7 @@ import Input from 'pl-fe/components/ui/input';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useDisableMfa } from 'pl-fe/queries/settings/use-mfa';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -21,22 +21,23 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const DisableOtpForm: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const { mutate: disableMfa, isPending } = useDisableMfa();
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
dispatch(disableMfa('totp', password)).then(() => {
|
||||
toast.success(intl.formatMessage(messages.mfaDisableSuccess));
|
||||
history.push('../auth/edit');
|
||||
}).finally(() => {
|
||||
setIsLoading(false);
|
||||
}).catch(() => {
|
||||
toast.error(intl.formatMessage(messages.disableFail));
|
||||
disableMfa(password, {
|
||||
onSuccess: () => {
|
||||
toast.success(intl.formatMessage(messages.mfaDisableSuccess));
|
||||
history.push('../auth/edit');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(intl.formatMessage(messages.disableFail));
|
||||
},
|
||||
});
|
||||
}, [password, dispatch, intl]);
|
||||
|
||||
@ -65,7 +66,7 @@ const DisableOtpForm: React.FC = () => {
|
||||
placeholder={intl.formatMessage(messages.passwordPlaceholder)}
|
||||
name='password'
|
||||
onChange={handleInputChange}
|
||||
disabled={isLoading}
|
||||
disabled={isPending}
|
||||
value={password}
|
||||
required
|
||||
/>
|
||||
@ -73,7 +74,7 @@ const DisableOtpForm: React.FC = () => {
|
||||
|
||||
<FormActions>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
disabled={isPending}
|
||||
theme='danger'
|
||||
type='submit'
|
||||
text={intl.formatMessage(messages.mfa_setup_disable_button)}
|
||||
|
||||
@ -2,13 +2,12 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { fetchBackupCodes } from 'pl-fe/actions/mfa';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import FormActions from 'pl-fe/components/ui/form-actions';
|
||||
import Spinner from 'pl-fe/components/ui/spinner';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useClient } from 'pl-fe/hooks/use-client';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -23,14 +22,14 @@ interface IEnableOtpForm {
|
||||
}
|
||||
|
||||
const EnableOtpForm: React.FC<IEnableOtpForm> = ({ displayOtpForm, handleSetupProceedClick }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const client = useClient();
|
||||
|
||||
const [backupCodes, setBackupCodes] = useState<Array<string>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchBackupCodes()).then(({ codes: backupCodes }) => {
|
||||
client.settings.mfa.getMfaBackupCodes().then(({ codes: backupCodes }) => {
|
||||
setBackupCodes(backupCodes);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { setupMfa, confirmMfa } from 'pl-fe/actions/mfa';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Form from 'pl-fe/components/ui/form';
|
||||
import FormActions from 'pl-fe/components/ui/form-actions';
|
||||
@ -11,8 +10,9 @@ import FormGroup from 'pl-fe/components/ui/form-group';
|
||||
import Input from 'pl-fe/components/ui/input';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useClient } from 'pl-fe/hooks/use-client';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useConfirmMfa } from 'pl-fe/queries/settings/use-mfa';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -28,19 +28,20 @@ const messages = defineMessages({
|
||||
const OtpConfirmForm: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const client = useClient();
|
||||
|
||||
const [state, setState] = useState<{ password: string; isLoading: boolean; code: string; qrCodeURI: string; confirmKey: string }>({
|
||||
const { mutate: confirmMfa, isPending } = useConfirmMfa();
|
||||
|
||||
const [state, setState] = useState<{ password: string; code: string; qrCodeURI: string; confirmKey: string }>({
|
||||
password: '',
|
||||
isLoading: false,
|
||||
code: '',
|
||||
qrCodeURI: '',
|
||||
confirmKey: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setupMfa('totp')).then((data) => {
|
||||
client.settings.mfa.getMfaSetup('totp').then((data) => {
|
||||
setState((prevState) => ({ ...prevState, qrCodeURI: data.provisioning_uri, confirmKey: data.key }));
|
||||
}).catch(() => {
|
||||
toast.error(intl.formatMessage(messages.qrFail));
|
||||
@ -56,12 +57,14 @@ const OtpConfirmForm: React.FC = () => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
setState((prevState) => ({ ...prevState, isLoading: true }));
|
||||
|
||||
dispatch(confirmMfa('totp', state.code, state.password)).then((r) => {
|
||||
toast.success(intl.formatMessage(messages.mfaConfirmSuccess));
|
||||
history.push('../auth/edit');
|
||||
}).catch(() => {
|
||||
toast.error(intl.formatMessage(messages.confirmFail));
|
||||
setState((prevState) => ({ ...prevState, isLoading: false }));
|
||||
confirmMfa(state, {
|
||||
onSuccess: () => {
|
||||
toast.success(intl.formatMessage(messages.mfaConfirmSuccess));
|
||||
history.push('../auth/edit');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(intl.formatMessage(messages.confirmFail));
|
||||
},
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
@ -96,7 +99,7 @@ const OtpConfirmForm: React.FC = () => {
|
||||
placeholder={intl.formatMessage(messages.codePlaceholder)}
|
||||
onChange={handleInputChange}
|
||||
autoComplete='off'
|
||||
disabled={state.isLoading}
|
||||
disabled={isPending}
|
||||
value={state.code}
|
||||
type='text'
|
||||
required
|
||||
@ -113,7 +116,7 @@ const OtpConfirmForm: React.FC = () => {
|
||||
name='password'
|
||||
placeholder={intl.formatMessage(messages.passwordPlaceholder)}
|
||||
onChange={handleInputChange}
|
||||
disabled={state.isLoading}
|
||||
disabled={isPending}
|
||||
value={state.password}
|
||||
required
|
||||
/>
|
||||
@ -126,14 +129,14 @@ const OtpConfirmForm: React.FC = () => {
|
||||
theme='tertiary'
|
||||
text={intl.formatMessage(messages.mfaCancelButton)}
|
||||
onClick={() => history.push('../auth/edit')}
|
||||
disabled={state.isLoading}
|
||||
disabled={isPending}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
theme='primary'
|
||||
text={intl.formatMessage(messages.mfaSetupConfirmButton)}
|
||||
disabled={state.isLoading}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</FormActions>
|
||||
</Form>
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchMfa } from 'pl-fe/actions/mfa';
|
||||
import List, { ListItem } from 'pl-fe/components/list';
|
||||
import Card, { CardBody, CardHeader, CardTitle } from 'pl-fe/components/ui/card';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Preferences from 'pl-fe/features/preferences';
|
||||
import MessagesSettings from 'pl-fe/features/settings/components/messages-settings';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import { useMfaConfig } from 'pl-fe/queries/settings/use-mfa';
|
||||
|
||||
const messages = defineMessages({
|
||||
accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' },
|
||||
@ -44,18 +42,13 @@ const messages = defineMessages({
|
||||
|
||||
/** User settings page. */
|
||||
const SettingsPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const mfa = useAppSelector((state) => state.security.mfa);
|
||||
const { data: mfa } = useMfaConfig();
|
||||
const features = useFeatures();
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const isMfaEnabled = mfa.settings?.totp;
|
||||
|
||||
useEffect(() => {
|
||||
if (features.manageMfa) dispatch(fetchMfa());
|
||||
}, [dispatch]);
|
||||
const isMfaEnabled = mfa?.settings.totp;
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
|
||||
50
packages/pl-fe/src/queries/settings/use-mfa.ts
Normal file
50
packages/pl-fe/src/queries/settings/use-mfa.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useClient } from 'pl-fe/hooks/use-client';
|
||||
|
||||
const useMfaConfig = () => {
|
||||
const client = useClient();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['settings', 'mfa'],
|
||||
queryFn: () => client.settings.mfa.getMfaSettings(),
|
||||
});
|
||||
};
|
||||
|
||||
const useConfirmMfa = () => {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['settings', 'mfa'],
|
||||
mutationFn: ({ code, password }: { code: string; password: string }) => client.settings.mfa.confirmMfaSetup('totp', code, password),
|
||||
onSuccess: () => {
|
||||
queryClient.setQueryData(['settings', 'mfa'], {
|
||||
settings: {
|
||||
enabled: true,
|
||||
totp: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const useDisableMfa = () => {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['settings', 'mfa'],
|
||||
mutationFn: (password: string) => client.settings.mfa.disableMfa('totp', password),
|
||||
onSuccess: () => {
|
||||
queryClient.setQueryData(['settings', 'mfa'], {
|
||||
settings: {
|
||||
enabled: false,
|
||||
totp: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { useMfaConfig, useConfirmMfa, useDisableMfa };
|
||||
@ -25,7 +25,6 @@ import pending_statuses from './pending-statuses';
|
||||
import plfe from './pl-fe';
|
||||
import polls from './polls';
|
||||
import push_notifications from './push-notifications';
|
||||
import security from './security';
|
||||
import shoutbox from './shoutbox';
|
||||
import status_lists from './status-lists';
|
||||
import statuses from './statuses';
|
||||
@ -54,7 +53,6 @@ const reducers = {
|
||||
plfe,
|
||||
polls,
|
||||
push_notifications,
|
||||
security,
|
||||
shoutbox,
|
||||
status_lists,
|
||||
statuses,
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import { create } from 'mutative';
|
||||
|
||||
import {
|
||||
MFA_FETCH_SUCCESS,
|
||||
MFA_CONFIRM_SUCCESS,
|
||||
MFA_DISABLE_SUCCESS,
|
||||
type MfaAction,
|
||||
} from '../actions/mfa';
|
||||
|
||||
interface State {
|
||||
mfa: {
|
||||
settings: Record<string, boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
mfa: {
|
||||
settings: {
|
||||
totp: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const importMfa = (state: State, data: any) => state.mfa = data;
|
||||
|
||||
const enableMfa = (state: State, method: string) => state.mfa.settings = { ...state.mfa.settings, [method]: true };
|
||||
|
||||
const disableMfa = (state: State, method: string) => state.mfa.settings = { ...state.mfa.settings, [method]: false };
|
||||
|
||||
const security = (state = initialState, action: MfaAction) => {
|
||||
switch (action.type) {
|
||||
case MFA_FETCH_SUCCESS:
|
||||
return create(state, (draft) => {
|
||||
importMfa(draft, action.data);
|
||||
});
|
||||
case MFA_CONFIRM_SUCCESS:
|
||||
return create(state, (draft) => {
|
||||
enableMfa(draft, action.method);
|
||||
});
|
||||
case MFA_DISABLE_SUCCESS:
|
||||
return create(state, (draft) => {
|
||||
disableMfa(draft, action.method);
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { security as default };
|
||||
Reference in New Issue
Block a user