refactored generate page
This commit is contained in:
parent
87ae9f8d31
commit
9eb793d606
22 changed files with 766 additions and 196 deletions
|
@ -1,130 +1,17 @@
|
|||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { configureSRP, logoutUser, putAttributes } from 'services/userService';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import GeneratePage from '@ente/accounts/pages/generate';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import {
|
||||
saveKeyInSessionStore,
|
||||
generateAndSaveIntermediateKeyAttributes,
|
||||
generateKeyAndSRPAttributes,
|
||||
} from 'utils/crypto';
|
||||
import SetPasswordForm from 'components/SetPasswordForm';
|
||||
import { justSignedUp, setJustSignedUp } from 'utils/storage';
|
||||
import RecoveryKey from 'components/RecoveryKey';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { VerticallyCentered } from 'components/Container';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { KeyAttributes, User } from 'types/user';
|
||||
import FormPaper from 'components/Form/FormPaper';
|
||||
import FormTitle from 'components/Form/FormPaper/Title';
|
||||
import { APPS, getAppName } from 'constants/apps';
|
||||
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
||||
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||
import { useContext } from 'react';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
|
||||
export default function Generate() {
|
||||
const [token, setToken] = useState<string>();
|
||||
const [user, setUser] = useState<User>();
|
||||
const router = useRouter();
|
||||
const [recoverModalView, setRecoveryModalView] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const appContext = useContext(AppContext);
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
const key: string = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
const keyAttributes: KeyAttributes = getData(
|
||||
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES
|
||||
);
|
||||
router.prefetch(PAGES.GALLERY);
|
||||
router.prefetch(PAGES.CREDENTIALS);
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
setUser(user);
|
||||
if (!user?.token) {
|
||||
router.push(PAGES.ROOT);
|
||||
} else if (key) {
|
||||
if (justSignedUp()) {
|
||||
setRecoveryModalView(true);
|
||||
setLoading(false);
|
||||
} else {
|
||||
const appName = getAppName();
|
||||
if (appName === APPS.AUTH) {
|
||||
router.push(PAGES.AUTH);
|
||||
} else {
|
||||
router.push(PAGES.GALLERY);
|
||||
}
|
||||
}
|
||||
} else if (keyAttributes?.encryptedKey) {
|
||||
router.push(PAGES.CREDENTIALS);
|
||||
} else {
|
||||
setToken(user.token);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
main();
|
||||
appContext.showNavBar(true);
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (passphrase, setFieldError) => {
|
||||
try {
|
||||
const { keyAttributes, masterKey, srpSetupAttributes } =
|
||||
await generateKeyAndSRPAttributes(passphrase);
|
||||
|
||||
await putAttributes(token, keyAttributes);
|
||||
await configureSRP(srpSetupAttributes);
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
keyAttributes,
|
||||
masterKey
|
||||
);
|
||||
await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, masterKey);
|
||||
setJustSignedUp(true);
|
||||
setRecoveryModalView(true);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to generate password');
|
||||
setFieldError('passphrase', t('PASSWORD_GENERATION_FAILED'));
|
||||
}
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
</VerticallyCentered>
|
||||
) : recoverModalView ? (
|
||||
<RecoveryKey
|
||||
show={recoverModalView}
|
||||
onHide={() => {
|
||||
setRecoveryModalView(false);
|
||||
const appName = getAppName();
|
||||
if (appName === APPS.AUTH) {
|
||||
router.push(PAGES.AUTH);
|
||||
} else {
|
||||
router.push(PAGES.GALLERY);
|
||||
}
|
||||
}}
|
||||
somethingWentWrong={() => null}
|
||||
/>
|
||||
) : (
|
||||
<VerticallyCentered>
|
||||
<FormPaper>
|
||||
<FormTitle>{t('SET_PASSPHRASE')}</FormTitle>
|
||||
<SetPasswordForm
|
||||
userEmail={user?.email}
|
||||
callback={onSubmit}
|
||||
buttonText={t('SET_PASSPHRASE')}
|
||||
/>
|
||||
<FormPaperFooter>
|
||||
<LinkButton onClick={logoutUser}>
|
||||
{t('GO_BACK')}
|
||||
</LinkButton>
|
||||
</FormPaperFooter>
|
||||
</FormPaper>
|
||||
</VerticallyCentered>
|
||||
)}
|
||||
</>
|
||||
<GeneratePage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.PHOTOS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -151,3 +151,9 @@ export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
|
|||
HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, null, {
|
||||
'X-Auth-Token': token,
|
||||
});
|
||||
|
||||
export const disableTwoFactor = async () => {
|
||||
await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
};
|
||||
|
|
83
packages/accounts/components/RecoveryKey/index.tsx
Normal file
83
packages/accounts/components/RecoveryKey/index.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { downloadAsFile } from '@ente/shared/utils';
|
||||
import { getRecoveryKey } from '@ente/shared/crypto/helpers';
|
||||
import CodeBlock from '@ente/shared/components/CodeBlock';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import * as bip39 from 'bip39';
|
||||
import { DashedBorderWrapper } from './styledComponents';
|
||||
import DialogTitleWithCloseButton from '@ente/shared/components/DialogBox/TitleWithCloseButton';
|
||||
import { t } from 'i18next';
|
||||
import { PageProps } from '@ente/shared/apps/types';
|
||||
|
||||
// mobile client library only supports english.
|
||||
bip39.setDefaultWordlist('english');
|
||||
|
||||
const RECOVERY_KEY_FILE_NAME = 'ente-recovery-key.txt';
|
||||
|
||||
interface Props {
|
||||
appContext: PageProps['appContext'];
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
somethingWentWrong: any;
|
||||
}
|
||||
|
||||
function RecoveryKey({ somethingWentWrong, appContext, ...props }: Props) {
|
||||
const [recoveryKey, setRecoveryKey] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.show) {
|
||||
return;
|
||||
}
|
||||
const main = async () => {
|
||||
try {
|
||||
const recoveryKey = await getRecoveryKey();
|
||||
setRecoveryKey(bip39.entropyToMnemonic(recoveryKey));
|
||||
} catch (e) {
|
||||
somethingWentWrong();
|
||||
props.onHide();
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, [props.show]);
|
||||
|
||||
function onSaveClick() {
|
||||
downloadAsFile(RECOVERY_KEY_FILE_NAME, recoveryKey);
|
||||
props.onHide();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
fullScreen={appContext.isMobile}
|
||||
open={props.show}
|
||||
onClose={props.onHide}
|
||||
maxWidth="xs">
|
||||
<DialogTitleWithCloseButton onClose={props.onHide}>
|
||||
{t('RECOVERY_KEY')}
|
||||
</DialogTitleWithCloseButton>
|
||||
<DialogContent>
|
||||
<Typography mb={3}>{t('RECOVERY_KEY_DESCRIPTION')}</Typography>
|
||||
<DashedBorderWrapper>
|
||||
<CodeBlock code={recoveryKey} />
|
||||
<Typography m={2}>
|
||||
{t('KEY_NOT_STORED_DISCLAIMER')}
|
||||
</Typography>
|
||||
</DashedBorderWrapper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="secondary" size="large" onClick={props.onHide}>
|
||||
{t('SAVE_LATER')}
|
||||
</Button>
|
||||
<Button color="accent" size="large" onClick={onSaveClick}>
|
||||
{t('SAVE')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default RecoveryKey;
|
|
@ -0,0 +1,6 @@
|
|||
import { Box, styled } from '@mui/material';
|
||||
|
||||
export const DashedBorderWrapper = styled(Box)(({ theme }) => ({
|
||||
border: `1px dashed ${theme.palette.grey.A400}`,
|
||||
borderRadius: theme.spacing(1),
|
||||
}));
|
|
@ -8,10 +8,8 @@ import {
|
|||
generateAndSaveIntermediateKeyAttributes,
|
||||
saveKeyInSessionStore,
|
||||
} from '@ente/shared/crypto/helpers';
|
||||
import {
|
||||
generateKeyAndSRPAttributes,
|
||||
isWeakPassword,
|
||||
} from '@ente/accounts/utils';
|
||||
import { isWeakPassword } from '@ente/accounts/utils';
|
||||
import { generateKeyAndSRPAttributes } from '@ente/accounts/utils/srp';
|
||||
|
||||
import { setJustSignedUp } from '@ente/shared/storage/localStorage/helpers';
|
||||
import { SESSION_KEYS } from '@ente/shared/storage/sessionStorage';
|
||||
|
|
130
packages/accounts/pages/generate.tsx
Normal file
130
packages/accounts/pages/generate.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { logoutUser } from '@ente/accounts/services/user';
|
||||
import { putAttributes } from '@ente/accounts/api/user';
|
||||
import { configureSRP } from '@ente/accounts/services/srp';
|
||||
import { getData, LS_KEYS } from '@ente/shared/storage/localStorage';
|
||||
import { getKey, SESSION_KEYS } from '@ente/shared/storage/sessionStorage';
|
||||
import {
|
||||
saveKeyInSessionStore,
|
||||
generateAndSaveIntermediateKeyAttributes,
|
||||
} from '@ente/shared/crypto/helpers';
|
||||
import { generateKeyAndSRPAttributes } from '@ente/accounts/utils/srp';
|
||||
|
||||
import SetPasswordForm from '@ente/accounts/components/SetPasswordForm';
|
||||
import {
|
||||
justSignedUp,
|
||||
setJustSignedUp,
|
||||
} from '@ente/shared/storage/localStorage/helpers';
|
||||
import RecoveryKey from '@ente/accounts/components/RecoveryKey';
|
||||
import { PAGES } from '@ente/accounts/constants/pages';
|
||||
import { VerticallyCentered } from '@ente/shared/components/Container';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { KeyAttributes, User } from '@ente/shared/user/types';
|
||||
import FormPaper from '@ente/shared/components/Form/FormPaper';
|
||||
import FormTitle from '@ente/shared/components/Form/FormPaper/Title';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
import FormPaperFooter from '@ente/shared/components/Form/FormPaper/Footer';
|
||||
import LinkButton from '@ente/shared/components/LinkButton';
|
||||
import { PageProps } from '@ente/shared/apps/types';
|
||||
|
||||
export default function Generate({ router, appContext, appName }: PageProps) {
|
||||
const [user, setUser] = useState<User>();
|
||||
const [recoverModalView, setRecoveryModalView] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
const key: string = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
const keyAttributes: KeyAttributes = getData(
|
||||
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES
|
||||
);
|
||||
router.prefetch(PAGES.GALLERY);
|
||||
router.prefetch(PAGES.CREDENTIALS);
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
setUser(user);
|
||||
if (!user?.token) {
|
||||
router.push(PAGES.ROOT);
|
||||
} else if (key) {
|
||||
if (justSignedUp()) {
|
||||
setRecoveryModalView(true);
|
||||
setLoading(false);
|
||||
} else {
|
||||
if (appName === APPS.AUTH) {
|
||||
router.push(PAGES.AUTH);
|
||||
} else {
|
||||
router.push(PAGES.GALLERY);
|
||||
}
|
||||
}
|
||||
} else if (keyAttributes?.encryptedKey) {
|
||||
router.push(PAGES.CREDENTIALS);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
main();
|
||||
appContext.showNavBar(true);
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (passphrase, setFieldError) => {
|
||||
try {
|
||||
const { keyAttributes, masterKey, srpSetupAttributes } =
|
||||
await generateKeyAndSRPAttributes(passphrase);
|
||||
|
||||
await putAttributes(keyAttributes);
|
||||
await configureSRP(srpSetupAttributes);
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
keyAttributes,
|
||||
masterKey
|
||||
);
|
||||
await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, masterKey);
|
||||
setJustSignedUp(true);
|
||||
setRecoveryModalView(true);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to generate password');
|
||||
setFieldError('passphrase', t('PASSWORD_GENERATION_FAILED'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
</VerticallyCentered>
|
||||
) : recoverModalView ? (
|
||||
<RecoveryKey
|
||||
appContext={appContext}
|
||||
show={recoverModalView}
|
||||
onHide={() => {
|
||||
setRecoveryModalView(false);
|
||||
if (appName === APPS.AUTH) {
|
||||
router.push(PAGES.AUTH);
|
||||
} else {
|
||||
router.push(PAGES.GALLERY);
|
||||
}
|
||||
}}
|
||||
somethingWentWrong={() => null}
|
||||
/>
|
||||
) : (
|
||||
<VerticallyCentered>
|
||||
<FormPaper>
|
||||
<FormTitle>{t('SET_PASSPHRASE')}</FormTitle>
|
||||
<SetPasswordForm
|
||||
userEmail={user?.email}
|
||||
callback={onSubmit}
|
||||
buttonText={t('SET_PASSPHRASE')}
|
||||
/>
|
||||
<FormPaperFooter>
|
||||
<LinkButton onClick={logoutUser}>
|
||||
{t('GO_BACK')}
|
||||
</LinkButton>
|
||||
</FormPaperFooter>
|
||||
</FormPaper>
|
||||
</VerticallyCentered>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -19,7 +19,7 @@ import { t } from 'i18next';
|
|||
import { Trans } from 'react-i18next';
|
||||
import { Link } from '@mui/material';
|
||||
import { SUPPORT_EMAIL } from '@ente/shared/constants/urls';
|
||||
import { DialogBoxAttributesV2 } from '@ente/shared/components/DialogBoxV2';
|
||||
import { DialogBoxAttributesV2 } from '@ente/shared/components/DialogBoxV2/types';
|
||||
import { ApiError } from '@ente/shared/error';
|
||||
import { HttpStatusCode } from 'axios';
|
||||
import { PageProps } from '@ente/shared/apps/types';
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
import { generateLoginSubKey } from '@ente/shared/crypto/helpers';
|
||||
import { KeyAttributes } from '@ente/shared/user/types';
|
||||
import { generateSRPSetupAttributes } from '../services/srp';
|
||||
import { SRPSetupAttributes } from '../types/srp';
|
||||
import ComlinkCryptoWorker from '@ente/shared/crypto';
|
||||
import { PasswordStrength } from '@ente/accounts/constants';
|
||||
import zxcvbn from 'zxcvbn';
|
||||
|
||||
|
@ -14,64 +9,6 @@ export const convertBase64ToBuffer = (base64: string) => {
|
|||
return Buffer.from(base64, 'base64');
|
||||
};
|
||||
|
||||
export async function generateKeyAndSRPAttributes(passphrase: string): Promise<{
|
||||
keyAttributes: KeyAttributes;
|
||||
masterKey: string;
|
||||
srpSetupAttributes: SRPSetupAttributes;
|
||||
}> {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const masterKey = await cryptoWorker.generateEncryptionKey();
|
||||
const recoveryKey = await cryptoWorker.generateEncryptionKey();
|
||||
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
|
||||
|
||||
const masterKeyEncryptedWithKek = await cryptoWorker.encryptToB64(
|
||||
masterKey,
|
||||
kek.key
|
||||
);
|
||||
const masterKeyEncryptedWithRecoveryKey = await cryptoWorker.encryptToB64(
|
||||
masterKey,
|
||||
recoveryKey
|
||||
);
|
||||
const recoveryKeyEncryptedWithMasterKey = await cryptoWorker.encryptToB64(
|
||||
recoveryKey,
|
||||
masterKey
|
||||
);
|
||||
|
||||
const keyPair = await cryptoWorker.generateKeyPair();
|
||||
const encryptedKeyPairAttributes = await cryptoWorker.encryptToB64(
|
||||
keyPair.privateKey,
|
||||
masterKey
|
||||
);
|
||||
|
||||
const loginSubKey = await generateLoginSubKey(kek.key);
|
||||
|
||||
const srpSetupAttributes = await generateSRPSetupAttributes(loginSubKey);
|
||||
|
||||
const keyAttributes: KeyAttributes = {
|
||||
kekSalt,
|
||||
encryptedKey: masterKeyEncryptedWithKek.encryptedData,
|
||||
keyDecryptionNonce: masterKeyEncryptedWithKek.nonce,
|
||||
publicKey: keyPair.publicKey,
|
||||
encryptedSecretKey: encryptedKeyPairAttributes.encryptedData,
|
||||
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce,
|
||||
opsLimit: kek.opsLimit,
|
||||
memLimit: kek.memLimit,
|
||||
masterKeyEncryptedWithRecoveryKey:
|
||||
masterKeyEncryptedWithRecoveryKey.encryptedData,
|
||||
masterKeyDecryptionNonce: masterKeyEncryptedWithRecoveryKey.nonce,
|
||||
recoveryKeyEncryptedWithMasterKey:
|
||||
recoveryKeyEncryptedWithMasterKey.encryptedData,
|
||||
recoveryKeyDecryptionNonce: recoveryKeyEncryptedWithMasterKey.nonce,
|
||||
};
|
||||
|
||||
return {
|
||||
keyAttributes,
|
||||
masterKey,
|
||||
srpSetupAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
export function estimatePasswordStrength(password: string): PasswordStrength {
|
||||
if (!password) {
|
||||
return PasswordStrength.WEAK;
|
||||
|
|
63
packages/accounts/utils/srp.ts
Normal file
63
packages/accounts/utils/srp.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { generateLoginSubKey } from '@ente/shared/crypto/helpers';
|
||||
import { KeyAttributes } from '@ente/shared/user/types';
|
||||
import { generateSRPSetupAttributes } from '../services/srp';
|
||||
import { SRPSetupAttributes } from '../types/srp';
|
||||
import ComlinkCryptoWorker from '@ente/shared/crypto';
|
||||
|
||||
export async function generateKeyAndSRPAttributes(passphrase: string): Promise<{
|
||||
keyAttributes: KeyAttributes;
|
||||
masterKey: string;
|
||||
srpSetupAttributes: SRPSetupAttributes;
|
||||
}> {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const masterKey = await cryptoWorker.generateEncryptionKey();
|
||||
const recoveryKey = await cryptoWorker.generateEncryptionKey();
|
||||
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
|
||||
|
||||
const masterKeyEncryptedWithKek = await cryptoWorker.encryptToB64(
|
||||
masterKey,
|
||||
kek.key
|
||||
);
|
||||
const masterKeyEncryptedWithRecoveryKey = await cryptoWorker.encryptToB64(
|
||||
masterKey,
|
||||
recoveryKey
|
||||
);
|
||||
const recoveryKeyEncryptedWithMasterKey = await cryptoWorker.encryptToB64(
|
||||
recoveryKey,
|
||||
masterKey
|
||||
);
|
||||
|
||||
const keyPair = await cryptoWorker.generateKeyPair();
|
||||
const encryptedKeyPairAttributes = await cryptoWorker.encryptToB64(
|
||||
keyPair.privateKey,
|
||||
masterKey
|
||||
);
|
||||
|
||||
const loginSubKey = await generateLoginSubKey(kek.key);
|
||||
|
||||
const srpSetupAttributes = await generateSRPSetupAttributes(loginSubKey);
|
||||
|
||||
const keyAttributes: KeyAttributes = {
|
||||
kekSalt,
|
||||
encryptedKey: masterKeyEncryptedWithKek.encryptedData,
|
||||
keyDecryptionNonce: masterKeyEncryptedWithKek.nonce,
|
||||
publicKey: keyPair.publicKey,
|
||||
encryptedSecretKey: encryptedKeyPairAttributes.encryptedData,
|
||||
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce,
|
||||
opsLimit: kek.opsLimit,
|
||||
memLimit: kek.memLimit,
|
||||
masterKeyEncryptedWithRecoveryKey:
|
||||
masterKeyEncryptedWithRecoveryKey.encryptedData,
|
||||
masterKeyDecryptionNonce: masterKeyEncryptedWithRecoveryKey.nonce,
|
||||
recoveryKeyEncryptedWithMasterKey:
|
||||
recoveryKeyEncryptedWithMasterKey.encryptedData,
|
||||
recoveryKeyDecryptionNonce: recoveryKeyEncryptedWithMasterKey.nonce,
|
||||
};
|
||||
|
||||
return {
|
||||
keyAttributes,
|
||||
masterKey,
|
||||
srpSetupAttributes,
|
||||
};
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import { NextRouter } from 'next/router';
|
||||
import { APPS } from './constants';
|
||||
import { SetDialogBoxAttributesV2 } from '@ente/shared/components/DialogBoxV2';
|
||||
import { SetDialogBoxAttributesV2 } from '@ente/shared/components/DialogBoxV2/types';
|
||||
|
||||
export interface PageProps {
|
||||
appContext: {
|
||||
showNavBar: (show: boolean) => void;
|
||||
isMobile: boolean;
|
||||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
||||
};
|
||||
router: NextRouter;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FreeFlowText } from '../Container';
|
||||
import { FreeFlowText } from '@ente/shared/components/Container';
|
||||
import React from 'react';
|
||||
import EnteSpinner from '../EnteSpinner';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import { Wrapper, CodeWrapper, CopyButtonWrapper } from './styledComponents';
|
||||
import CopyButton from './CopyButton';
|
||||
import { BoxProps } from '@mui/material';
|
||||
|
|
18
packages/shared/components/DialogBox/DialogIcon.tsx
Normal file
18
packages/shared/components/DialogBox/DialogIcon.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export default function DialogIcon({ icon }: { icon: React.ReactNode }) {
|
||||
return (
|
||||
<Box
|
||||
className="DialogIcon"
|
||||
sx={{
|
||||
svg: {
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
},
|
||||
color: 'stroke.muted',
|
||||
}}>
|
||||
{icon}
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
DialogProps,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { SpaceBetweenFlex } from '@ente/shared/components/Container';
|
||||
|
||||
const DialogTitleWithCloseButton = (props) => {
|
||||
const { children, onClose, ...other } = props;
|
||||
|
||||
return (
|
||||
<DialogTitle {...other}>
|
||||
<SpaceBetweenFlex>
|
||||
<Typography variant="h3" fontWeight={'bold'}>
|
||||
{children}
|
||||
</Typography>
|
||||
{onClose && (
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={onClose}
|
||||
sx={{ float: 'right' }}
|
||||
color="secondary">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</SpaceBetweenFlex>
|
||||
</DialogTitle>
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogTitleWithCloseButton;
|
||||
|
||||
export const dialogCloseHandler =
|
||||
({
|
||||
staticBackdrop,
|
||||
nonClosable,
|
||||
onClose,
|
||||
}: {
|
||||
staticBackdrop?: boolean;
|
||||
nonClosable?: boolean;
|
||||
onClose: () => void;
|
||||
}): DialogProps['onClose'] =>
|
||||
(_, reason) => {
|
||||
if (nonClosable) {
|
||||
// no-op
|
||||
} else if (staticBackdrop && reason === 'backdropClick') {
|
||||
// no-op
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
40
packages/shared/components/DialogBox/base.tsx
Normal file
40
packages/shared/components/DialogBox/base.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Dialog, styled } from '@mui/material';
|
||||
|
||||
const DialogBoxBase = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialog-paper': {
|
||||
padding: theme.spacing(1, 1.5),
|
||||
maxWidth: '346px',
|
||||
},
|
||||
|
||||
'& .DialogIcon': {
|
||||
padding: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
|
||||
'& .MuiDialogTitle-root': {
|
||||
padding: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
|
||||
'.DialogIcon + .MuiDialogTitle-root': {
|
||||
paddingTop: 0,
|
||||
},
|
||||
|
||||
'.MuiDialogTitle-root + .MuiDialogContent-root': {
|
||||
paddingTop: 0,
|
||||
},
|
||||
'.MuiDialogTitle-root + .MuiDialogActions-root': {
|
||||
paddingTop: theme.spacing(3),
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
flexWrap: 'wrap-reverse',
|
||||
},
|
||||
'& .MuiButton-root': {
|
||||
margin: `${theme.spacing(0.5, 0)} !important`,
|
||||
},
|
||||
}));
|
||||
|
||||
export default DialogBoxBase;
|
116
packages/shared/components/DialogBox/index.tsx
Normal file
116
packages/shared/components/DialogBox/index.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import DialogTitleWithCloseButton, {
|
||||
dialogCloseHandler,
|
||||
} from './TitleWithCloseButton';
|
||||
import DialogBoxBase from './base';
|
||||
import { DialogBoxAttributes } from './types';
|
||||
import DialogIcon from './DialogIcon';
|
||||
import { t } from 'i18next';
|
||||
|
||||
type IProps = React.PropsWithChildren<
|
||||
Omit<DialogProps, 'onClose' | 'maxSize'> & {
|
||||
onClose: () => void;
|
||||
attributes: DialogBoxAttributes;
|
||||
size?: Breakpoint;
|
||||
titleCloseButton?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export default function DialogBox({
|
||||
attributes,
|
||||
children,
|
||||
open,
|
||||
size,
|
||||
onClose,
|
||||
titleCloseButton,
|
||||
...props
|
||||
}: IProps) {
|
||||
if (!attributes) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleClose = dialogCloseHandler({
|
||||
staticBackdrop: attributes.staticBackdrop,
|
||||
nonClosable: attributes.nonClosable,
|
||||
onClose: onClose,
|
||||
});
|
||||
|
||||
return (
|
||||
<DialogBoxBase
|
||||
open={open}
|
||||
maxWidth={size}
|
||||
onClose={handleClose}
|
||||
{...props}>
|
||||
{attributes.icon && <DialogIcon icon={attributes.icon} />}
|
||||
{attributes.title && (
|
||||
<DialogTitleWithCloseButton
|
||||
onClose={
|
||||
titleCloseButton &&
|
||||
!attributes.nonClosable &&
|
||||
handleClose
|
||||
}>
|
||||
{attributes.title}
|
||||
</DialogTitleWithCloseButton>
|
||||
)}
|
||||
{(children || attributes?.content) && (
|
||||
<DialogContent>
|
||||
{children || (
|
||||
<Typography color="text.muted">
|
||||
{attributes.content}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogContent>
|
||||
)}
|
||||
{(attributes.close || attributes.proceed) && (
|
||||
<DialogActions>
|
||||
<>
|
||||
{attributes.close && (
|
||||
<Button
|
||||
size="large"
|
||||
color={attributes.close?.variant ?? 'secondary'}
|
||||
onClick={() => {
|
||||
attributes.close.action &&
|
||||
attributes.close?.action();
|
||||
onClose();
|
||||
}}>
|
||||
{attributes.close?.text ?? t('OK')}
|
||||
</Button>
|
||||
)}
|
||||
{attributes.proceed && (
|
||||
<Button
|
||||
size="large"
|
||||
color={attributes.proceed?.variant}
|
||||
onClick={() => {
|
||||
attributes.proceed.action();
|
||||
onClose();
|
||||
}}
|
||||
disabled={attributes.proceed.disabled}>
|
||||
{attributes.proceed.text}
|
||||
</Button>
|
||||
)}
|
||||
{attributes.secondary && (
|
||||
<Button
|
||||
size="large"
|
||||
color={attributes.secondary?.variant}
|
||||
onClick={() => {
|
||||
attributes.secondary.action();
|
||||
onClose();
|
||||
}}
|
||||
disabled={attributes.secondary.disabled}>
|
||||
{attributes.secondary.text}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</DialogActions>
|
||||
)}
|
||||
</DialogBoxBase>
|
||||
);
|
||||
}
|
30
packages/shared/components/DialogBox/types.ts
Normal file
30
packages/shared/components/DialogBox/types.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { ButtonProps } from '@mui/material';
|
||||
|
||||
export interface DialogBoxAttributes {
|
||||
icon?: React.ReactNode;
|
||||
title?: string;
|
||||
staticBackdrop?: boolean;
|
||||
nonClosable?: boolean;
|
||||
content?: any;
|
||||
close?: {
|
||||
text?: string;
|
||||
variant?: ButtonProps['color'];
|
||||
action?: () => void;
|
||||
};
|
||||
proceed?: {
|
||||
text: string;
|
||||
action: () => void;
|
||||
variant?: ButtonProps['color'];
|
||||
disabled?: boolean;
|
||||
};
|
||||
secondary?: {
|
||||
text: string;
|
||||
action: () => void;
|
||||
variant: ButtonProps['color'];
|
||||
disabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type SetDialogBoxAttributes = React.Dispatch<
|
||||
React.SetStateAction<DialogBoxAttributes>
|
||||
>;
|
135
packages/shared/components/DialogBoxV2/index.tsx
Normal file
135
packages/shared/components/DialogBoxV2/index.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogProps,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { t } from 'i18next';
|
||||
import { dialogCloseHandler } from '@ente/shared/components/DialogBox/TitleWithCloseButton';
|
||||
import { DialogBoxAttributesV2 } from './types';
|
||||
import EnteButton from '@ente/shared/components/EnteButton';
|
||||
|
||||
type IProps = React.PropsWithChildren<
|
||||
Omit<DialogProps, 'onClose'> & {
|
||||
onClose: () => void;
|
||||
attributes: DialogBoxAttributesV2;
|
||||
}
|
||||
>;
|
||||
|
||||
export default function DialogBoxV2({
|
||||
attributes,
|
||||
children,
|
||||
open,
|
||||
onClose,
|
||||
...props
|
||||
}: IProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
if (!attributes) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleClose = dialogCloseHandler({
|
||||
staticBackdrop: attributes.staticBackdrop,
|
||||
nonClosable: attributes.nonClosable,
|
||||
onClose: onClose,
|
||||
});
|
||||
|
||||
const { PaperProps, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
PaperProps={{
|
||||
...PaperProps,
|
||||
sx: {
|
||||
padding: '8px 12px',
|
||||
maxWidth: '360px',
|
||||
...PaperProps?.sx,
|
||||
},
|
||||
}}
|
||||
{...rest}>
|
||||
<Stack spacing={'36px'} p={'16px'}>
|
||||
<Stack spacing={'19px'}>
|
||||
{attributes.icon && (
|
||||
<Box
|
||||
sx={{
|
||||
'& > svg': {
|
||||
fontSize: '32px',
|
||||
},
|
||||
}}>
|
||||
{attributes.icon}
|
||||
</Box>
|
||||
)}
|
||||
{attributes.title && (
|
||||
<Typography variant="large" fontWeight={'bold'}>
|
||||
{attributes.title}
|
||||
</Typography>
|
||||
)}
|
||||
{children ||
|
||||
(attributes?.content && (
|
||||
<Typography color="text.muted">
|
||||
{attributes.content}
|
||||
</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
{(attributes.proceed ||
|
||||
attributes.close ||
|
||||
attributes.buttons?.length) && (
|
||||
<Stack
|
||||
spacing={'8px'}
|
||||
direction={
|
||||
attributes.buttonDirection === 'row'
|
||||
? 'row-reverse'
|
||||
: 'column'
|
||||
}
|
||||
flex={1}>
|
||||
{attributes.proceed && (
|
||||
<EnteButton
|
||||
loading={loading}
|
||||
size="large"
|
||||
color={attributes.proceed?.variant}
|
||||
onClick={async () => {
|
||||
await attributes.proceed.action(setLoading);
|
||||
|
||||
onClose();
|
||||
}}
|
||||
disabled={attributes.proceed.disabled}>
|
||||
{attributes.proceed.text}
|
||||
</EnteButton>
|
||||
)}
|
||||
{attributes.close && (
|
||||
<Button
|
||||
size="large"
|
||||
color={attributes.close?.variant ?? 'secondary'}
|
||||
onClick={() => {
|
||||
attributes.close.action &&
|
||||
attributes.close?.action();
|
||||
onClose();
|
||||
}}>
|
||||
{attributes.close?.text ?? t('OK')}
|
||||
</Button>
|
||||
)}
|
||||
{attributes.buttons &&
|
||||
attributes.buttons.map((b) => (
|
||||
<Button
|
||||
size="large"
|
||||
key={b.text}
|
||||
color={b.variant}
|
||||
onClick={() => {
|
||||
b.action();
|
||||
onClose();
|
||||
}}
|
||||
disabled={b.disabled}>
|
||||
{b.text}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
47
packages/shared/components/EnteButton.tsx
Normal file
47
packages/shared/components/EnteButton.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import Done from '@mui/icons-material/Done';
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
CircularProgress,
|
||||
PaletteColor,
|
||||
} from '@mui/material';
|
||||
|
||||
interface Iprops extends ButtonProps {
|
||||
loading?: boolean;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
export default function EnteButton({
|
||||
children,
|
||||
loading,
|
||||
success,
|
||||
disabled,
|
||||
sx,
|
||||
...props
|
||||
}: Iprops) {
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
...sx,
|
||||
...((loading || success) && {
|
||||
'&.Mui-disabled': (theme) => ({
|
||||
backgroundColor: (
|
||||
theme.palette[props.color] as PaletteColor
|
||||
).main,
|
||||
color: (theme.palette[props.color] as PaletteColor)
|
||||
.contrastText,
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
{...props}>
|
||||
{loading ? (
|
||||
<CircularProgress size={20} sx={{ color: 'inherit' }} />
|
||||
) : success ? (
|
||||
<Done sx={{ fontSize: 20 }} />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { FC } from 'react';
|
||||
import { BoxProps, Divider } from '@mui/material';
|
||||
import { VerticallyCentered } from '../../Container';
|
||||
import { VerticallyCentered } from '@ente/shared/components/Container';
|
||||
|
||||
const FormPaperFooter: FC<BoxProps> = ({ sx, style, ...props }) => {
|
||||
return (
|
||||
|
|
|
@ -3,8 +3,8 @@ import { Formik, FormikHelpers, FormikState } from 'formik';
|
|||
import * as Yup from 'yup';
|
||||
import SubmitButton from './SubmitButton';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import ShowHidePassword from './Form/ShowHidePassword';
|
||||
import { FlexWrapper } from './Container';
|
||||
import ShowHidePassword from '@ente/shared/components/Form/ShowHidePassword';
|
||||
import { FlexWrapper } from '@ente/shared/components/Container';
|
||||
import { Button, FormHelperText } from '@mui/material';
|
||||
import { t } from 'i18next';
|
||||
|
||||
|
|
|
@ -3,3 +3,22 @@ export async function sleep(time: number) {
|
|||
setTimeout(() => resolve(null), time);
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadAsFile(filename: string, content: string) {
|
||||
const file = new Blob([content], {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
downloadUsingAnchor(fileURL, filename);
|
||||
}
|
||||
|
||||
export function downloadUsingAnchor(link: string, name: string) {
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = link;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(link);
|
||||
a.remove();
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue