revert commit and migrate code to use i18n
This commit is contained in:
parent
4885f198e5
commit
5ddd80a080
|
@ -9,6 +9,7 @@ export enum PAGES {
|
||||||
SIGNUP = '/signup',
|
SIGNUP = '/signup',
|
||||||
TWO_FACTOR_SETUP = '/two-factor/setup',
|
TWO_FACTOR_SETUP = '/two-factor/setup',
|
||||||
TWO_FACTOR_VERIFY = '/two-factor/verify',
|
TWO_FACTOR_VERIFY = '/two-factor/verify',
|
||||||
|
TWO_FACTOR_RECOVER = '/two-factor/recover',
|
||||||
VERIFY = '/verify',
|
VERIFY = '/verify',
|
||||||
ROOT = '/',
|
ROOT = '/',
|
||||||
SHARED_ALBUMS = '/shared-albums',
|
SHARED_ALBUMS = '/shared-albums',
|
||||||
|
|
132
src/pages/two-factor/recover/index.tsx
Normal file
132
src/pages/two-factor/recover/index.tsx
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import SingleInputForm, {
|
||||||
|
SingleInputFormProps,
|
||||||
|
} from 'components/SingleInputForm';
|
||||||
|
import VerticallyCentered from 'components/Container';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
||||||
|
import { AppContext } from 'pages/_app';
|
||||||
|
import { PAGES } from 'constants/pages';
|
||||||
|
import FormPaper from 'components/Form/FormPaper';
|
||||||
|
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
||||||
|
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
||||||
|
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||||
|
import { B64EncryptionResult } from 'types/crypto';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
import { Link } from '@mui/material';
|
||||||
|
import { SUPPORT_EMAIL } from 'constants/urls';
|
||||||
|
|
||||||
|
const bip39 = require('bip39');
|
||||||
|
// mobile client library only supports english.
|
||||||
|
bip39.setDefaultWordlist('english');
|
||||||
|
|
||||||
|
export default function Recover() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
|
||||||
|
useState<B64EncryptionResult>(null);
|
||||||
|
const [sessionID, setSessionID] = useState(null);
|
||||||
|
const appContext = useContext(AppContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
router.prefetch(PAGES.GALLERY);
|
||||||
|
const user = getData(LS_KEYS.USER);
|
||||||
|
if (!user.isTwoFactorEnabled && (user.encryptedToken || user.token)) {
|
||||||
|
router.push(PAGES.GENERATE);
|
||||||
|
} else if (!user.email || !user.twoFactorSessionID) {
|
||||||
|
router.push(PAGES.ROOT);
|
||||||
|
} else {
|
||||||
|
setSessionID(user.twoFactorSessionID);
|
||||||
|
}
|
||||||
|
const main = async () => {
|
||||||
|
const resp = await recoverTwoFactor(user.twoFactorSessionID);
|
||||||
|
setEncryptedTwoFactorSecret({
|
||||||
|
encryptedData: resp.encryptedSecret,
|
||||||
|
nonce: resp.secretDecryptionNonce,
|
||||||
|
key: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
main();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const recover: SingleInputFormProps['callback'] = async (
|
||||||
|
recoveryKey: string,
|
||||||
|
setFieldError
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
recoveryKey = recoveryKey
|
||||||
|
.trim()
|
||||||
|
.split(' ')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter((part) => !!part)
|
||||||
|
.join(' ');
|
||||||
|
// check if user is entering mnemonic recovery key
|
||||||
|
if (recoveryKey.indexOf(' ') > 0) {
|
||||||
|
if (recoveryKey.split(' ').length !== 24) {
|
||||||
|
throw new Error('recovery code should have 24 words');
|
||||||
|
}
|
||||||
|
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||||
|
}
|
||||||
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
|
const twoFactorSecret = await cryptoWorker.decryptB64(
|
||||||
|
encryptedTwoFactorSecret.encryptedData,
|
||||||
|
encryptedTwoFactorSecret.nonce,
|
||||||
|
await cryptoWorker.fromHex(recoveryKey)
|
||||||
|
);
|
||||||
|
const resp = await removeTwoFactor(sessionID, twoFactorSecret);
|
||||||
|
const { keyAttributes, encryptedToken, token, id } = resp;
|
||||||
|
setData(LS_KEYS.USER, {
|
||||||
|
...getData(LS_KEYS.USER),
|
||||||
|
token,
|
||||||
|
encryptedToken,
|
||||||
|
id,
|
||||||
|
isTwoFactorEnabled: false,
|
||||||
|
});
|
||||||
|
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
|
||||||
|
router.push(PAGES.CREDENTIALS);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'two factor recovery failed');
|
||||||
|
setFieldError(t('INCORRECT_RECOVERY_KEY'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showNoRecoveryKeyMessage = () => {
|
||||||
|
appContext.setDialogMessage({
|
||||||
|
title: t('CONTACT_SUPPORT'),
|
||||||
|
close: {},
|
||||||
|
content: (
|
||||||
|
<Trans
|
||||||
|
i18nKey={'NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE'}
|
||||||
|
components={{
|
||||||
|
a: <Link href={`mailto:${SUPPORT_EMAIL}`} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VerticallyCentered>
|
||||||
|
<FormPaper>
|
||||||
|
<FormPaperTitle>{t('RECOVER_TWO_FACTOR')}</FormPaperTitle>
|
||||||
|
<SingleInputForm
|
||||||
|
callback={recover}
|
||||||
|
fieldType="text"
|
||||||
|
placeholder={t('RECOVERY_KEY_HINT')}
|
||||||
|
buttonText={t('RECOVER')}
|
||||||
|
/>
|
||||||
|
<FormPaperFooter style={{ justifyContent: 'space-between' }}>
|
||||||
|
<LinkButton onClick={showNoRecoveryKeyMessage}>
|
||||||
|
{t('NO_RECOVERY_KEY')}
|
||||||
|
</LinkButton>
|
||||||
|
<LinkButton onClick={router.back}>
|
||||||
|
{t('GO_BACK')}
|
||||||
|
</LinkButton>
|
||||||
|
</FormPaperFooter>
|
||||||
|
</FormPaper>
|
||||||
|
</VerticallyCentered>
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { useRouter } from 'next/router';
|
||||||
import VerifyTwoFactor, {
|
import VerifyTwoFactor, {
|
||||||
VerifyTwoFactorCallback,
|
VerifyTwoFactorCallback,
|
||||||
} from 'components/TwoFactor/VerifyForm';
|
} from 'components/TwoFactor/VerifyForm';
|
||||||
|
import { encryptWithRecoveryKey } from 'utils/crypto';
|
||||||
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
||||||
import { PAGES } from 'constants/pages';
|
import { PAGES } from 'constants/pages';
|
||||||
import { TwoFactorSecret } from 'types/user';
|
import { TwoFactorSecret } from 'types/user';
|
||||||
|
@ -45,7 +46,10 @@ export default function SetupTwoFactor() {
|
||||||
otp: string,
|
otp: string,
|
||||||
markSuccessful
|
markSuccessful
|
||||||
) => {
|
) => {
|
||||||
await enableTwoFactor(otp);
|
const recoveryEncryptedTwoFactorSecret = await encryptWithRecoveryKey(
|
||||||
|
twoFactorSecret.secretCode
|
||||||
|
);
|
||||||
|
await enableTwoFactor(otp, recoveryEncryptedTwoFactorSecret);
|
||||||
await markSuccessful();
|
await markSuccessful();
|
||||||
setData(LS_KEYS.USER, {
|
setData(LS_KEYS.USER, {
|
||||||
...getData(LS_KEYS.USER),
|
...getData(LS_KEYS.USER),
|
||||||
|
|
|
@ -2,13 +2,11 @@ import VerifyTwoFactor, {
|
||||||
VerifyTwoFactorCallback,
|
VerifyTwoFactorCallback,
|
||||||
} from 'components/TwoFactor/VerifyForm';
|
} from 'components/TwoFactor/VerifyForm';
|
||||||
import router from 'next/router';
|
import router from 'next/router';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { logoutUser, verifyTwoFactor } from 'services/userService';
|
import { logoutUser, verifyTwoFactor } from 'services/userService';
|
||||||
import { AppContext } from 'pages/_app';
|
|
||||||
import { PAGES } from 'constants/pages';
|
import { PAGES } from 'constants/pages';
|
||||||
import { User } from 'types/user';
|
import { User } from 'types/user';
|
||||||
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
||||||
import { Trans } from 'react-i18next';
|
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
|
||||||
import LinkButton from 'components/pages/gallery/LinkButton';
|
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||||
|
@ -16,12 +14,9 @@ import FormContainer from 'components/Form/FormContainer';
|
||||||
import FormPaper from 'components/Form/FormPaper';
|
import FormPaper from 'components/Form/FormPaper';
|
||||||
import FormTitle from 'components/Form/FormPaper/Title';
|
import FormTitle from 'components/Form/FormPaper/Title';
|
||||||
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
||||||
import { Link } from '@mui/material';
|
|
||||||
import { SUPPORT_EMAIL } from 'constants/urls';
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [sessionID, setSessionID] = useState('');
|
const [sessionID, setSessionID] = useState('');
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
@ -41,22 +36,6 @@ export default function Home() {
|
||||||
main();
|
main();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const showContactSupport = () => {
|
|
||||||
appContext.setDialogMessage({
|
|
||||||
title: t('CONTACT_SUPPORT'),
|
|
||||||
close: {},
|
|
||||||
content: (
|
|
||||||
<Trans
|
|
||||||
i18nKey="NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE"
|
|
||||||
components={{
|
|
||||||
a: <Link href={`mailto:${SUPPORT_EMAIL}`} />,
|
|
||||||
}}
|
|
||||||
values={{ emailID: SUPPORT_EMAIL }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit: VerifyTwoFactorCallback = async (otp) => {
|
const onSubmit: VerifyTwoFactorCallback = async (otp) => {
|
||||||
try {
|
try {
|
||||||
const resp = await verifyTwoFactor(otp, sessionID);
|
const resp = await verifyTwoFactor(otp, sessionID);
|
||||||
|
@ -84,7 +63,8 @@ export default function Home() {
|
||||||
<VerifyTwoFactor onSubmit={onSubmit} buttonText={t('VERIFY')} />
|
<VerifyTwoFactor onSubmit={onSubmit} buttonText={t('VERIFY')} />
|
||||||
|
|
||||||
<FormPaperFooter>
|
<FormPaperFooter>
|
||||||
<LinkButton onClick={showContactSupport}>
|
<LinkButton
|
||||||
|
onClick={() => router.push(PAGES.TWO_FACTOR_RECOVER)}>
|
||||||
{t('LOST_DEVICE')}
|
{t('LOST_DEVICE')}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</FormPaperFooter>
|
</FormPaperFooter>
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
RecoveryKey,
|
RecoveryKey,
|
||||||
TwoFactorSecret,
|
TwoFactorSecret,
|
||||||
TwoFactorVerificationResponse,
|
TwoFactorVerificationResponse,
|
||||||
|
TwoFactorRecoveryResponse,
|
||||||
UserDetails,
|
UserDetails,
|
||||||
DeleteChallengeResponse,
|
DeleteChallengeResponse,
|
||||||
GetRemoteStoreValueResponse,
|
GetRemoteStoreValueResponse,
|
||||||
|
@ -23,6 +24,7 @@ import { ServerErrorCodes } from 'utils/error';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import safeStorageService from './electron/safeStorage';
|
import safeStorageService from './electron/safeStorage';
|
||||||
import { deleteAllCache } from 'utils/storage/cache';
|
import { deleteAllCache } from 'utils/storage/cache';
|
||||||
|
import { B64EncryptionResult } from 'types/crypto';
|
||||||
import { getLocalFamilyData, isPartOfFamily } from 'utils/user/family';
|
import { getLocalFamilyData, isPartOfFamily } from 'utils/user/family';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
@ -217,11 +219,18 @@ export const setupTwoFactor = async () => {
|
||||||
return resp.data as TwoFactorSecret;
|
return resp.data as TwoFactorSecret;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enableTwoFactor = async (code: string) => {
|
export const enableTwoFactor = async (
|
||||||
|
code: string,
|
||||||
|
recoveryEncryptedTwoFactorSecret: B64EncryptionResult
|
||||||
|
) => {
|
||||||
await HTTPService.post(
|
await HTTPService.post(
|
||||||
`${ENDPOINT}/users/two-factor/enable`,
|
`${ENDPOINT}/users/two-factor/enable`,
|
||||||
{
|
{
|
||||||
code,
|
code,
|
||||||
|
encryptedTwoFactorSecret:
|
||||||
|
recoveryEncryptedTwoFactorSecret.encryptedData,
|
||||||
|
twoFactorSecretDecryptionNonce:
|
||||||
|
recoveryEncryptedTwoFactorSecret.nonce,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
|
@ -242,6 +251,21 @@ export const verifyTwoFactor = async (code: string, sessionID: string) => {
|
||||||
return resp.data as TwoFactorVerificationResponse;
|
return resp.data as TwoFactorVerificationResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const recoverTwoFactor = async (sessionID: string) => {
|
||||||
|
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/recover`, {
|
||||||
|
sessionID,
|
||||||
|
});
|
||||||
|
return resp.data as TwoFactorRecoveryResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeTwoFactor = async (sessionID: string, secret: string) => {
|
||||||
|
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
|
||||||
|
sessionID,
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
return resp.data as TwoFactorVerificationResponse;
|
||||||
|
};
|
||||||
|
|
||||||
export const disableTwoFactor = async () => {
|
export const disableTwoFactor = async () => {
|
||||||
await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
|
await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
|
|
|
@ -61,6 +61,11 @@ export interface TwoFactorSecret {
|
||||||
qrCode: string;
|
qrCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TwoFactorRecoveryResponse {
|
||||||
|
encryptedSecret: string;
|
||||||
|
secretDecryptionNonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FamilyMember {
|
export interface FamilyMember {
|
||||||
email: string;
|
email: string;
|
||||||
usage: number;
|
usage: number;
|
||||||
|
|
Loading…
Reference in a new issue