commit
6f5aa9de0e
|
@ -42,6 +42,7 @@
|
|||
"react-burger-menu": "^3.0.4",
|
||||
"react-dom": "16.13.1",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-otp-input": "^2.3.1",
|
||||
"react-select": "^4.3.1",
|
||||
"react-top-loading-bar": "^2.0.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
|
|
|
@ -91,7 +91,7 @@ function CollectionShare(props: Props) {
|
|||
}}
|
||||
onClick={() => collectionUnshare(sharee)}
|
||||
>
|
||||
-
|
||||
-
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -103,7 +103,8 @@ function CollectionShare(props: Props) {
|
|||
attributes={{ title: constants.SHARE_COLLECTION }}
|
||||
>
|
||||
<DeadCenter style={{ width: '85%', margin: 'auto' }}>
|
||||
<p>{constants.SHARE_WITH_PEOPLE}</p>
|
||||
<h6>{constants.SHARE_WITH_PEOPLE}</h6>
|
||||
<p />
|
||||
<Formik<formValues>
|
||||
initialValues={{ email: '' }}
|
||||
validationSchema={Yup.object().shape({
|
||||
|
|
19
src/components/FlashMessageBar.tsx
Normal file
19
src/components/FlashMessageBar.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { FlashMessage } from 'pages/_app';
|
||||
import React from 'react';
|
||||
import Alert from 'react-bootstrap/Alert';
|
||||
|
||||
|
||||
export default function FlashMessageBar({ flashMessage, onClose }: { flashMessage: FlashMessage, onClose: () => void }) {
|
||||
return (
|
||||
<Alert
|
||||
className="flash-message text-center"
|
||||
variant={flashMessage.severity}
|
||||
dismissible
|
||||
onClose={onClose}
|
||||
>
|
||||
<div style={{ maxWidth: '1024px', width: '80%', margin: 'auto' }}>
|
||||
{flashMessage.message}
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
|
@ -52,7 +52,7 @@ export default function MessageDialog({
|
|||
</Modal.Header>
|
||||
{(children || attributes?.content) && (
|
||||
<Modal.Body style={{ borderTop: '1px solid #444' }}>
|
||||
{children || <h5>{attributes.content}</h5>}
|
||||
{children || <p style={{ fontSize: '1.25rem', marginBottom: 0 }}>{attributes.content}</p>}
|
||||
</Modal.Body>
|
||||
)}
|
||||
{(attributes.close || attributes.proceed) && (
|
||||
|
@ -67,8 +67,7 @@ export default function MessageDialog({
|
|||
<Button
|
||||
variant={`outline-${attributes.close?.variant ?? 'secondary'}`}
|
||||
onClick={() => {
|
||||
attributes.close?.action && attributes.close?.action();
|
||||
props.onHide();
|
||||
attributes.close?.action ? attributes.close?.action() : props.onHide();
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 3em',
|
||||
|
|
|
@ -62,7 +62,7 @@ const ListItem = styled.div`
|
|||
justify-content: center;
|
||||
`;
|
||||
|
||||
const getTemplateColumns = (columns: number, groups?: number[]):string => {
|
||||
const getTemplateColumns = (columns: number, groups?: number[]): string => {
|
||||
if (groups) {
|
||||
const sum = groups.reduce((acc, item) => acc + item, 0);
|
||||
if (sum < columns) {
|
||||
|
@ -82,12 +82,12 @@ const ListContainer = styled.div<{ columns: number, groups?: number[] }>`
|
|||
width: 100%;
|
||||
color: #fff;
|
||||
|
||||
@media(max-width: ${IMAGE_CONTAINER_MAX_WIDTH*4}px) {
|
||||
@media(max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const DateContainer = styled.div<{span: number}>`
|
||||
const DateContainer = styled.div<{ span: number }>`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -97,7 +97,7 @@ const DateContainer = styled.div<{span: number}>`
|
|||
height: ${DATE_CONTAINER_HEIGHT}px;
|
||||
`;
|
||||
|
||||
const BannerContainer = styled.div<{span: number}>`
|
||||
const BannerContainer = styled.div<{ span: number }>`
|
||||
color: #979797;
|
||||
text-align: center;
|
||||
grid-column: span ${(props) => props.span};
|
||||
|
@ -293,7 +293,7 @@ const PhotoFrame = ({
|
|||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
video.addEventListener('timeupdate', function() {
|
||||
video.addEventListener('timeupdate', function () {
|
||||
clearTimeout(t);
|
||||
resolve(null);
|
||||
});
|
||||
|
@ -396,8 +396,8 @@ const PhotoFrame = ({
|
|||
|
||||
const isSameDay = (first, second) => (
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate()
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate()
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -472,11 +472,11 @@ const PhotoFrame = ({
|
|||
{!isFirstLoad && files.length === 0 && !searchMode ? (
|
||||
<EmptyScreen>
|
||||
<img height={150} src='/images/gallery.png' />
|
||||
<br/>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
onClick={openFileUploader}
|
||||
style={{
|
||||
marginTop: '32px',
|
||||
paddingLeft: '32px',
|
||||
paddingRight: '32px',
|
||||
paddingTop: '12px',
|
||||
|
|
|
@ -4,7 +4,26 @@ import { getRecoveryKey } from 'utils/crypto';
|
|||
import constants from 'utils/strings/constants';
|
||||
import MessageDialog from './MessageDialog';
|
||||
import EnteSpinner from './EnteSpinner';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CodeBlock = styled.div<{ height: number }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1a1919;
|
||||
height: ${(props) => props.height}px;
|
||||
padding-left:30px;
|
||||
padding-right:20px;
|
||||
color: white;
|
||||
margin: 20px 0;
|
||||
width:100%;
|
||||
`;
|
||||
|
||||
export const FreeFlowText = styled.div`
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
min-width: 30%;
|
||||
`;
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
|
@ -55,34 +74,17 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
|
|||
}}
|
||||
>
|
||||
<p>{constants.RECOVERY_KEY_DESCRIPTION}</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#1a1919',
|
||||
height: '150px',
|
||||
padding: '40px',
|
||||
color: 'white',
|
||||
margin: '20px 0',
|
||||
}}
|
||||
>
|
||||
<CodeBlock height={150}>
|
||||
{recoveryKey ? (
|
||||
<div
|
||||
style={{
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
minWidth: '30%',
|
||||
}}
|
||||
>
|
||||
<FreeFlowText>
|
||||
{recoveryKey}
|
||||
</div>
|
||||
</FreeFlowText>
|
||||
) : (
|
||||
<EnteSpinner />
|
||||
)}
|
||||
</div>
|
||||
</CodeBlock>
|
||||
<p>{constants.KEY_NOT_STORED_DISCLAIMER}</p>
|
||||
</MessageDialog>
|
||||
</MessageDialog >
|
||||
);
|
||||
}
|
||||
export default RecoveryKeyModal;
|
||||
|
|
|
@ -28,11 +28,14 @@ import { LogoImage } from 'pages/_app';
|
|||
import { SetDialogMessage } from './MessageDialog';
|
||||
import EnteSpinner from './EnteSpinner';
|
||||
import RecoveryKeyModal from './RecoveryKeyModal';
|
||||
import TwoFactorModal from './TwoFactorModal';
|
||||
import { SetLoading } from 'pages/gallery';
|
||||
|
||||
interface Props {
|
||||
files: File[];
|
||||
collections: Collection[];
|
||||
setDialogMessage: SetDialogMessage;
|
||||
setLoading: SetLoading,
|
||||
showPlanSelectorModal: () => void;
|
||||
}
|
||||
export default function Sidebar(props: Props) {
|
||||
|
@ -45,6 +48,7 @@ export default function Sidebar(props: Props) {
|
|||
}, []);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [recoverModalView, setRecoveryModalView] = useState(false);
|
||||
const [twoFactorModalView, setTwoFactorModalView] = useState(false);
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
if (!isOpen) {
|
||||
|
@ -214,11 +218,26 @@ export default function Sidebar(props: Props) {
|
|||
{constants.DOWNLOAD_RECOVERY_KEY}
|
||||
</LinkButton>
|
||||
</>
|
||||
<>
|
||||
<TwoFactorModal
|
||||
show={twoFactorModalView}
|
||||
onHide={() => setTwoFactorModalView(false)}
|
||||
setDialogMessage={props.setDialogMessage}
|
||||
closeSidebar={() => setIsOpen(false)}
|
||||
setLoading={props.setLoading}
|
||||
/>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() => setTwoFactorModalView(true)}
|
||||
>
|
||||
{constants.TWO_FACTOR}
|
||||
</LinkButton>
|
||||
</>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() => {
|
||||
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
|
||||
router.push('changePassword');
|
||||
router.push('change-password');
|
||||
}}
|
||||
>
|
||||
{constants.CHANGE_PASSWORD}
|
||||
|
|
|
@ -22,7 +22,7 @@ const SubmitButton = ({
|
|||
<Spinner
|
||||
as="span"
|
||||
animation="border"
|
||||
style={{ width: '22px', height: '22px', borderWidth: '0.20em' }}
|
||||
style={{ width: '22px', height: '22px', borderWidth: '0.20em', color: '#2dc262' }}
|
||||
/>
|
||||
) : (
|
||||
buttonText
|
||||
|
|
123
src/components/TwoFactorModal.tsx
Normal file
123
src/components/TwoFactorModal.tsx
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import { DeadCenter, SetLoading } from 'pages/gallery';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { disableTwoFactor, getTwoFactorStatus } from 'services/userService';
|
||||
import styled from 'styled-components';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
import constants from 'utils/strings/constants';
|
||||
import MessageDialog, { SetDialogMessage } from './MessageDialog';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
setDialogMessage: SetDialogMessage;
|
||||
setLoading: SetLoading
|
||||
closeSidebar: () => void;
|
||||
}
|
||||
|
||||
const Row = styled.div`
|
||||
display:flex;
|
||||
align-items:center;
|
||||
margin-bottom:20px;
|
||||
flex:1
|
||||
`;
|
||||
|
||||
const Label = styled.div`
|
||||
width:70%;
|
||||
`;
|
||||
function TwoFactorModal(props: Props) {
|
||||
const router = useRouter();
|
||||
const [isTwoFactorEnabled, setTwoFactorStatus] = useState(false);
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.show) {
|
||||
return;
|
||||
}
|
||||
const isTwoFactorEnabled = getData(LS_KEYS.USER).isTwoFactorEnabled ?? false;
|
||||
setTwoFactorStatus(isTwoFactorEnabled);
|
||||
const main = async () => {
|
||||
const isTwoFactorEnabled = await getTwoFactorStatus();
|
||||
setTwoFactorStatus(isTwoFactorEnabled);
|
||||
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: false });
|
||||
};
|
||||
main();
|
||||
}, [props.show]);
|
||||
const warnTwoFactorDisable = async () => {
|
||||
props.setDialogMessage({
|
||||
title: constants.DISABLE_TWO_FACTOR,
|
||||
staticBackdrop: true,
|
||||
content: constants.DISABLE_TWO_FACTOR_MESSAGE,
|
||||
close: { text: constants.CANCEL },
|
||||
proceed: {
|
||||
variant: 'danger',
|
||||
text: constants.DISABLE,
|
||||
action: twoFactorDisable,
|
||||
},
|
||||
});
|
||||
};
|
||||
const twoFactorDisable = async () => {
|
||||
try {
|
||||
await disableTwoFactor();
|
||||
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: false });
|
||||
props.onHide();
|
||||
props.closeSidebar();
|
||||
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_DISABLE_SUCCESS, severity: 'info' });
|
||||
} catch (e) {
|
||||
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_DISABLE_FAILED, severity: 'danger' });
|
||||
}
|
||||
};
|
||||
const warnTwoFactorReconfigure = async () => {
|
||||
props.setDialogMessage({
|
||||
title: constants.UPDATE_TWO_FACTOR,
|
||||
staticBackdrop: true,
|
||||
content: constants.UPDATE_TWO_FACTOR_MESSAGE,
|
||||
close: { text: constants.CANCEL },
|
||||
proceed: {
|
||||
variant: 'success',
|
||||
text: constants.UPDATE,
|
||||
action: reconfigureTwoFactor,
|
||||
},
|
||||
});
|
||||
};
|
||||
const reconfigureTwoFactor = async () => {
|
||||
router.push('/two-factor/setup');
|
||||
};
|
||||
return (
|
||||
<MessageDialog
|
||||
show={props.show}
|
||||
onHide={props.onHide}
|
||||
attributes={{
|
||||
title: constants.TWO_FACTOR_AUTHENTICATION,
|
||||
staticBackdrop: true,
|
||||
}}
|
||||
|
||||
>
|
||||
<div {...(!isTwoFactorEnabled ? { style: { padding: '10px 10px 30px 10px' } } : { style: { padding: '10px' } })}>
|
||||
{
|
||||
isTwoFactorEnabled ?
|
||||
<>
|
||||
<Row>
|
||||
<Label>{constants.UPDATE_TWO_FACTOR_HINT}</Label> <Button variant={'outline-success'} style={{ width: '30%' }} onClick={warnTwoFactorReconfigure}>{constants.RECONFIGURE}</Button>
|
||||
</Row>
|
||||
<Row>
|
||||
<Label>{constants.DISABLE_TWO_FACTOR_HINT} </Label><Button variant={'outline-danger'} style={{ width: '30%' }} onClick={warnTwoFactorDisable}>{constants.DISABLE}</Button>
|
||||
</Row>
|
||||
|
||||
</> : (
|
||||
<DeadCenter>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="36px" viewBox="0 0 24 24" width="36px" fill="#000000"><g fill="none"><path d="M0 0h24v24H0V0z" /><path d="M0 0h24v24H0V0z" opacity=".87" /></g><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z" /></svg>
|
||||
<p />
|
||||
<p>{constants.TWO_FACTOR_INFO}</p>
|
||||
<div style={{ height: '10px' }} />
|
||||
<Button variant="outline-success" onClick={() => router.push('/two-factor/setup')}>{constants.ENABLE_TWO_FACTOR}</Button>
|
||||
</DeadCenter>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</MessageDialog >
|
||||
);
|
||||
}
|
||||
export default TwoFactorModal;
|
96
src/components/VerifyTwoFactor.tsx
Normal file
96
src/components/VerifyTwoFactor.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { DeadCenter } from 'pages/gallery';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Form } from 'react-bootstrap';
|
||||
import OtpInput from 'react-otp-input';
|
||||
import constants from 'utils/strings/constants';
|
||||
import SubmitButton from './SubmitButton';
|
||||
|
||||
interface formValues {
|
||||
otp: string;
|
||||
}
|
||||
interface Props {
|
||||
onSubmit: any
|
||||
back: any
|
||||
buttonText: string;
|
||||
}
|
||||
|
||||
export default function VerifyTwoFactor(props: Props) {
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
const otpInputRef = useRef(null);
|
||||
const submitForm = async (
|
||||
{ otp }: formValues,
|
||||
{ setFieldError, resetForm }: FormikHelpers<formValues>,
|
||||
) => {
|
||||
try {
|
||||
setWaiting(true);
|
||||
await props.onSubmit(otp);
|
||||
} catch (e) {
|
||||
resetForm();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
otpInputRef.current?.focusPrevInput();
|
||||
}
|
||||
setFieldError('otp', `${constants.UNKNOWN_ERROR} ${e.message}`);
|
||||
}
|
||||
setWaiting(false);
|
||||
};
|
||||
|
||||
const onChange = (otp: string, callback: Function, triggerSubmit: Function) => {
|
||||
callback(otp);
|
||||
if (otp.length === 6) {
|
||||
triggerSubmit(otp);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<p style={{ marginBottom: '30px' }}>enter the 6-digit code from your authenticator app.</p>
|
||||
<Formik<formValues>
|
||||
initialValues={{ otp: '' }}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={submitForm}
|
||||
>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
submitForm,
|
||||
}) => (
|
||||
<Form noValidate onSubmit={handleSubmit} style={{ width: '100%' }}>
|
||||
<Form.Group style={{ marginBottom: '32px' }} controlId="formBasicEmail">
|
||||
<DeadCenter>
|
||||
<OtpInput
|
||||
placeholder="123456"
|
||||
ref={otpInputRef}
|
||||
shouldAutoFocus
|
||||
value={values.otp}
|
||||
onChange={(otp) => {
|
||||
onChange(otp, handleChange('otp'), submitForm);
|
||||
}}
|
||||
numInputs={6}
|
||||
separator={'-'}
|
||||
isInputNum
|
||||
className={'otp-input'}
|
||||
/>
|
||||
{errors.otp &&
|
||||
<div style={{ display: 'block', marginTop: '16px' }} className="invalid-feedback">{constants.INCORRECT_CODE}</div>
|
||||
}
|
||||
</DeadCenter>
|
||||
</Form.Group>
|
||||
<SubmitButton
|
||||
buttonText={props.buttonText}
|
||||
loading={waiting}
|
||||
disabled={values.otp.length < 6}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -13,6 +13,7 @@ import { Workbox } from 'workbox-window';
|
|||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import HTTPService from 'services/HTTPService';
|
||||
import FlashMessageBar from 'components/FlashMessageBar';
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
/* ubuntu-regular - latin */
|
||||
|
@ -46,6 +47,9 @@ const GlobalStyles = createGlobalStyle`
|
|||
color: #aaa;
|
||||
font-family:Ubuntu, Arial, sans-serif !important;
|
||||
}
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
color: #d7d7d7;
|
||||
}
|
||||
|
||||
#__next {
|
||||
flex: 1;
|
||||
|
@ -161,11 +165,14 @@ const GlobalStyles = createGlobalStyle`
|
|||
background-size: 20px 20px;
|
||||
background-position: center;
|
||||
}
|
||||
.btn.focus , .btn:focus{
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-success {
|
||||
background: #2dc262;
|
||||
border-color: #29a354;
|
||||
}
|
||||
.btn-success:hover ,.btn-success:focus .btn-success:active{
|
||||
.btn-success:hover .btn-success:focus .btn-success:active {
|
||||
background-color: #29a354;
|
||||
border-color: #2dc262;
|
||||
}
|
||||
|
@ -177,12 +184,19 @@ const GlobalStyles = createGlobalStyle`
|
|||
border-color: #2dc262;
|
||||
border-width: 2px;
|
||||
}
|
||||
.btn-outline-success:hover {
|
||||
.btn-outline-success:hover:enabled {
|
||||
background: #2dc262;
|
||||
color: white;
|
||||
}
|
||||
.btn-outline-danger, .btn-outline-secondary, .btn-outline-primary{
|
||||
border-width: 2px;
|
||||
}
|
||||
.btn-link-danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
.btn-link-danger:hover {
|
||||
color: #ff495a;
|
||||
}
|
||||
.card {
|
||||
background-color: #242424;
|
||||
color: #d1d1d1;
|
||||
|
@ -194,11 +208,11 @@ const GlobalStyles = createGlobalStyle`
|
|||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.alert-success {
|
||||
.alert-primary {
|
||||
background-color: rgb(235, 255, 243);
|
||||
color: #000000;
|
||||
}
|
||||
.alert-primary {
|
||||
.alert-success {
|
||||
background-color: #c4ffd6;
|
||||
}
|
||||
.bm-burger-button {
|
||||
|
@ -314,6 +328,28 @@ const GlobalStyles = createGlobalStyle`
|
|||
.carousel-indicators .active {
|
||||
background-color: #2dc262;
|
||||
}
|
||||
div.otp-input input {
|
||||
width: 36px !important;
|
||||
height: 36px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
div.otp-input input::placeholder {
|
||||
opacity:0;
|
||||
}
|
||||
|
||||
div.otp-input input:not(:placeholder-shown) , div.otp-input input:focus{
|
||||
border: 2px solid #2dc262;
|
||||
border-radius:1px;
|
||||
-webkit-transition: 0.5s;
|
||||
transition: 0.5s;
|
||||
outline: none;
|
||||
}
|
||||
.flash-message{
|
||||
padding:16px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LogoImage = styled.img`
|
||||
|
@ -344,8 +380,13 @@ type AppContextType = {
|
|||
showNavBar: (show: boolean) => void;
|
||||
sharedFiles: File[];
|
||||
resetSharedFiles: () => void;
|
||||
setDisappearingFlashMessage: (message: FlashMessage) => void;
|
||||
}
|
||||
|
||||
export interface FlashMessage {
|
||||
message: string;
|
||||
severity: string
|
||||
}
|
||||
export const AppContext = createContext<AppContextType>(null);
|
||||
|
||||
const redirectMap = {
|
||||
|
@ -361,6 +402,7 @@ export default function App({ Component, err }) {
|
|||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
const [sharedFiles, setSharedFiles] = useState<File[]>(null);
|
||||
const [redirectName, setRedirectName] = useState<string>(null);
|
||||
const [flashMessage, setFlashMessage] = useState<FlashMessage>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
@ -438,9 +480,11 @@ export default function App({ Component, err }) {
|
|||
window.removeEventListener('offline', setUserOffline);
|
||||
};
|
||||
}, [redirectName]);
|
||||
|
||||
const showNavBar = (show: boolean) => setShowNavBar(show);
|
||||
|
||||
const setDisappearingFlashMessage = (flashMessages: FlashMessage) => {
|
||||
setFlashMessage(flashMessages);
|
||||
setTimeout(() => setFlashMessage(null), 5000);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
@ -470,10 +514,12 @@ export default function App({ Component, err }) {
|
|||
(router.pathname === '/gallery' ?
|
||||
<MessageContainer>{constants.FILES_TO_BE_UPLOADED(sharedFiles.length)}</MessageContainer> :
|
||||
<MessageContainer>{constants.LOGIN_TO_UPLOAD_FILES(sharedFiles.length)}</MessageContainer>)}
|
||||
{flashMessage && <FlashMessageBar flashMessage={flashMessage} onClose={() => setFlashMessage(null)} />}
|
||||
<AppContext.Provider value={{
|
||||
showNavBar,
|
||||
sharedFiles,
|
||||
resetSharedFiles,
|
||||
setDisappearingFlashMessage,
|
||||
}}>
|
||||
{loading ? (
|
||||
<Container>
|
||||
|
|
|
@ -67,7 +67,6 @@ export const DeadCenter = styled.div`
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
@ -426,6 +425,7 @@ export default function Gallery() {
|
|||
files={files}
|
||||
collections={collections}
|
||||
setDialogMessage={setDialogMessage}
|
||||
setLoading={setLoading}
|
||||
showPlanSelectorModal={() => setPlanModalView(true)}
|
||||
/>
|
||||
<UploadButton isFirstFetch={isFirstFetch} openFileUploader={openFileUploader} />
|
||||
|
|
113
src/pages/two-factor/recover/index.tsx
Normal file
113
src/pages/two-factor/recover/index.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
import { useRouter } from 'next/router';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import SingleInputForm from 'components/SingleInputForm';
|
||||
import MessageDialog from 'components/MessageDialog';
|
||||
import Container from 'components/Container';
|
||||
import { Card, Button } from 'react-bootstrap';
|
||||
import LogoImg from 'components/LogoImg';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { B64EncryptionResult } from 'services/uploadService';
|
||||
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
||||
|
||||
export default function Recover() {
|
||||
const router = useRouter();
|
||||
const [messageDialogView, SetMessageDialogView] = useState(false);
|
||||
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] = useState<B64EncryptionResult>(null);
|
||||
const [sessionID, setSessionID] = useState(null);
|
||||
useEffect(() => {
|
||||
router.prefetch('/gallery');
|
||||
const user = getData(LS_KEYS.USER);
|
||||
if (!user?.email) {
|
||||
router.push('/');
|
||||
}
|
||||
setSessionID(user.twoFactorSessionID);
|
||||
const main = async () => {
|
||||
const resp = await recoverTwoFactor(user.twoFactorSessionID);
|
||||
setEncryptedTwoFactorSecret({
|
||||
encryptedData: resp.encryptedSecret,
|
||||
nonce: resp.secretDecryptionNonce,
|
||||
key: null,
|
||||
});
|
||||
};
|
||||
main();
|
||||
}, []);
|
||||
|
||||
const recover = async (recoveryKey: string, setFieldError) => {
|
||||
try {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const twoFactorSecret: string = 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('/credentials');
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
setFieldError('passphrase', constants.INCORRECT_RECOVERY_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Card
|
||||
style={{ minWidth: '320px' }}
|
||||
className="text-center"
|
||||
>
|
||||
<Card.Body style={{ padding: '40px 30px' }}>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src='/icon.svg' />
|
||||
{constants.RECOVER_TWO_FACTOR}
|
||||
</Card.Title>
|
||||
<SingleInputForm
|
||||
callback={recover}
|
||||
fieldType="text"
|
||||
placeholder={constants.RETURN_RECOVERY_KEY_HINT}
|
||||
buttonText={constants.RECOVER}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => SetMessageDialogView(true)}
|
||||
>
|
||||
{constants.NO_RECOVERY_KEY}
|
||||
</Button>
|
||||
<Button variant="link" onClick={router.back}>
|
||||
{constants.GO_BACK}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Container>
|
||||
<MessageDialog
|
||||
size="lg"
|
||||
show={messageDialogView}
|
||||
onHide={() => SetMessageDialogView(false)}
|
||||
attributes={{
|
||||
title: constants.CONTACT_SUPPORT,
|
||||
close: {},
|
||||
content: constants.NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE(),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
111
src/pages/two-factor/setup/index.tsx
Normal file
111
src/pages/two-factor/setup/index.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import LogoImg from 'components/LogoImg';
|
||||
import { CodeBlock, FreeFlowText } from 'components/RecoveryKeyModal';
|
||||
import { DeadCenter } from 'pages/gallery';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button, Card } from 'react-bootstrap';
|
||||
import { enableTwoFactor, setupTwoFactor, TwoFactorSecret } from 'services/userService';
|
||||
import styled from 'styled-components';
|
||||
import constants from 'utils/strings/constants';
|
||||
import Container from 'components/Container';
|
||||
import { useRouter } from 'next/router';
|
||||
import VerifyTwoFactor from 'components/VerifyTwoFactor';
|
||||
import { B64EncryptionResult } from 'services/uploadService';
|
||||
import { encryptWithRecoveryKey } from 'utils/crypto';
|
||||
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
||||
import { AppContext } from 'pages/_app';
|
||||
|
||||
|
||||
enum SetupMode {
|
||||
QR_CODE,
|
||||
MANUAL_CODE,
|
||||
}
|
||||
|
||||
const QRCode = styled.img`
|
||||
height:200px;
|
||||
width:200px;
|
||||
margin:1rem;
|
||||
`;
|
||||
|
||||
export default function SetupTwoFactor() {
|
||||
const [setupMode, setSetupMode] = useState<SetupMode>(SetupMode.QR_CODE);
|
||||
const [twoFactorSecret, setTwoFactorSecret] = useState<TwoFactorSecret>(null);
|
||||
const [recoveryEncryptedTwoFactorSecret, setRecoveryEncryptedTwoFactorSecret] = useState<B64EncryptionResult>(null);
|
||||
const router = useRouter();
|
||||
const appContext = useContext(AppContext);
|
||||
useEffect(() => {
|
||||
if (twoFactorSecret) {
|
||||
return;
|
||||
}
|
||||
const main = async () => {
|
||||
try {
|
||||
const twoFactorSecret = await setupTwoFactor();
|
||||
const recoveryEncryptedTwoFactorSecret = await encryptWithRecoveryKey(twoFactorSecret.secretCode);
|
||||
setTwoFactorSecret(twoFactorSecret);
|
||||
setRecoveryEncryptedTwoFactorSecret(recoveryEncryptedTwoFactorSecret);
|
||||
} catch (e) {
|
||||
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_SETUP_FAILED, severity: 'danger' });
|
||||
router.push('/gallery');
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, []);
|
||||
const onSubmit = async (otp: string) => {
|
||||
await enableTwoFactor(otp, recoveryEncryptedTwoFactorSecret);
|
||||
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: true });
|
||||
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_SETUP_SUCCESS, severity: 'info' });
|
||||
router.push('/gallery');
|
||||
};
|
||||
return (
|
||||
<Container>
|
||||
<Card style={{ minWidth: '300px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
|
||||
<DeadCenter>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src='/icon.svg' />
|
||||
{constants.TWO_FACTOR}
|
||||
</Card.Title>
|
||||
{setupMode === SetupMode.QR_CODE ? (
|
||||
<>
|
||||
<p>{constants.TWO_FACTOR_QR_INSTRUCTION}</p>
|
||||
<DeadCenter>
|
||||
{!twoFactorSecret ? <div style={{ height: '200px', width: '200px', margin: '1rem', display: 'flex', justifyContent: 'center', alignItems: 'center', border: '1px solid #aaa' }}><EnteSpinner /></div> :
|
||||
<QRCode src={`data:image/png;base64,${twoFactorSecret.qrCode}`} />
|
||||
}
|
||||
<Button block variant="link" onClick={() => setSetupMode(SetupMode.MANUAL_CODE)}>
|
||||
{constants.ENTER_CODE_MANUALLY}
|
||||
</Button>
|
||||
</DeadCenter>
|
||||
</>
|
||||
) : (<>
|
||||
<p>{constants.TWO_FACTOR_MANUAL_CODE_INSTRUCTION}</p>
|
||||
<CodeBlock height={100}>
|
||||
{!twoFactorSecret ? <EnteSpinner /> : (
|
||||
<FreeFlowText>
|
||||
{twoFactorSecret.secretCode}
|
||||
</FreeFlowText>
|
||||
|
||||
)}
|
||||
</CodeBlock>
|
||||
<Button block variant="link" style={{ marginBottom: '1rem' }} onClick={() => setSetupMode(SetupMode.QR_CODE)}>
|
||||
{constants.SCAN_QR_CODE}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginBottom: '20px',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
<VerifyTwoFactor onSubmit={onSubmit} back={router.back} buttonText={constants.ENABLE} />
|
||||
<Button style={{ marginTop: '16px' }} variant="link-danger" onClick={router.back}>
|
||||
{constants.GO_BACK}
|
||||
</Button>
|
||||
</DeadCenter>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Container >
|
||||
);
|
||||
}
|
80
src/pages/two-factor/verify/index.tsx
Normal file
80
src/pages/two-factor/verify/index.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import Container from 'components/Container';
|
||||
import LogoImg from 'components/LogoImg';
|
||||
import VerifyTwoFactor from 'components/VerifyTwoFactor';
|
||||
import router from 'next/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Card } from 'react-bootstrap';
|
||||
import { logoutUser, verifyTwoFactor } from 'services/userService';
|
||||
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
export default function Home() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [sessionID, setSessionID] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
router.prefetch('/credentials');
|
||||
const user = getData(LS_KEYS.USER);
|
||||
if (!user?.email) {
|
||||
router.push('/');
|
||||
} else {
|
||||
setEmail(user.email);
|
||||
setSessionID(user.twoFactorSessionID);
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (otp: string) => {
|
||||
try {
|
||||
const resp = await verifyTwoFactor(otp, sessionID);
|
||||
const { keyAttributes, encryptedToken, token, id } = resp;
|
||||
setData(LS_KEYS.USER, {
|
||||
...getData(LS_KEYS.USER),
|
||||
email,
|
||||
token,
|
||||
encryptedToken,
|
||||
id,
|
||||
});
|
||||
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
|
||||
router.push('/credentials');
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
logoutUser();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Container>
|
||||
<Card style={{ minWidth: '300px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src='/icon.svg' />
|
||||
{constants.TWO_FACTOR}
|
||||
</Card.Title>
|
||||
<VerifyTwoFactor onSubmit={onSubmit} back={router.back} buttonText={constants.VERIFY} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => router.push('/two-factor/recover')}
|
||||
>
|
||||
{constants.LOST_DEVICE}
|
||||
</Button>
|
||||
<Button variant="link" onClick={logoutUser}>
|
||||
{constants.GO_BACK}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
|
@ -13,7 +13,7 @@ import {
|
|||
logoutUser,
|
||||
clearFiles,
|
||||
isTokenValid,
|
||||
VerificationResponse,
|
||||
EmailVerificationResponse,
|
||||
} from 'services/userService';
|
||||
import { setIsFirstLogin } from 'utils/storage';
|
||||
import SubmitButton from 'components/SubmitButton';
|
||||
|
@ -34,6 +34,7 @@ export default function Verify() {
|
|||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
router.prefetch('/twoFactor/verify');
|
||||
router.prefetch('/credentials');
|
||||
router.prefetch('/generate');
|
||||
const user = getData(LS_KEYS.USER);
|
||||
|
@ -60,7 +61,12 @@ export default function Verify() {
|
|||
try {
|
||||
setLoading(true);
|
||||
const resp = await verifyOtt(email, ott);
|
||||
const { keyAttributes, encryptedToken, token, id } = resp.data as VerificationResponse;
|
||||
const { keyAttributes, encryptedToken, token, id, twoFactorSessionID } = resp.data as EmailVerificationResponse;
|
||||
if (twoFactorSessionID) {
|
||||
setData(LS_KEYS.USER, { email, twoFactorSessionID, isTwoFactorEnabled: true });
|
||||
router.push('/two-factor/verify');
|
||||
return;
|
||||
}
|
||||
setData(LS_KEYS.USER, {
|
||||
...getData(LS_KEYS.USER),
|
||||
email,
|
||||
|
|
|
@ -6,6 +6,7 @@ import { clearData } from 'utils/storage/localStorage';
|
|||
import localForage from 'utils/storage/localForage';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import HTTPService from './HTTPService';
|
||||
import { B64EncryptionResult } from './uploadService';
|
||||
|
||||
export interface UpdatedKey {
|
||||
kekSalt: string;
|
||||
|
@ -28,13 +29,31 @@ export interface User {
|
|||
name: string;
|
||||
email: string;
|
||||
}
|
||||
export interface VerificationResponse {
|
||||
export interface EmailVerificationResponse {
|
||||
id: number;
|
||||
keyAttributes?: KeyAttributes;
|
||||
encryptedToken?: string;
|
||||
token?: string;
|
||||
twoFactorSessionID: string
|
||||
}
|
||||
|
||||
export interface TwoFactorVerificationResponse {
|
||||
id: number;
|
||||
keyAttributes: KeyAttributes;
|
||||
encryptedToken?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorSecret {
|
||||
secretCode: string
|
||||
qrCode: string
|
||||
}
|
||||
|
||||
export interface TwoFactorRecoveryResponse {
|
||||
encryptedSecret: string
|
||||
secretDecryptionNonce: string
|
||||
}
|
||||
|
||||
export const getOtt = (email: string) => HTTPService.get(`${ENDPOINT}/users/ott`, {
|
||||
email,
|
||||
client: 'web',
|
||||
|
@ -98,3 +117,54 @@ export const isTokenValid = async () => {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const setupTwoFactor = async () => {
|
||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/setup`, null, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
return resp.data as TwoFactorSecret;
|
||||
};
|
||||
|
||||
export const enableTwoFactor = async (code: string, recoveryEncryptedTwoFactorSecret: B64EncryptionResult) => {
|
||||
await HTTPService.post(`${ENDPOINT}/users/two-factor/enable`, {
|
||||
code,
|
||||
encryptedTwoFactorSecret: recoveryEncryptedTwoFactorSecret.encryptedData,
|
||||
twoFactorSecretDecryptionNonce: recoveryEncryptedTwoFactorSecret.nonce,
|
||||
}, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyTwoFactor = async (code: string, sessionID: string) => {
|
||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/verify`, {
|
||||
code, sessionID,
|
||||
}, null);
|
||||
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 () => {
|
||||
await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getTwoFactorStatus = async () => {
|
||||
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/status`, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
return resp.data['status'];
|
||||
};
|
||||
|
|
|
@ -167,4 +167,12 @@ export async function decryptAndStoreToken(masterKey: string) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function encryptWithRecoveryKey(key: string) {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const hexRecoveryKey = await getRecoveryKey();
|
||||
const recoveryKey = await cryptoWorker.fromHex(hexRecoveryKey);
|
||||
const encryptedKey: B64EncryptionResult = await cryptoWorker.encryptToB64(key, recoveryKey);
|
||||
return encryptedKey;
|
||||
}
|
||||
export default CryptoWorker;
|
||||
|
|
|
@ -5,7 +5,7 @@ import styled from 'styled-components';
|
|||
* Global English constants.
|
||||
*/
|
||||
|
||||
const dateString = function(date) {
|
||||
const dateString = function (date) {
|
||||
return new Date(date / 1000).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
|
@ -24,7 +24,7 @@ const Logo = styled.img`
|
|||
`;
|
||||
|
||||
const englishConstants = {
|
||||
HERO_HEADER: () => <div>with <Logo src='/icon.svg' /><br/>your <Strong>memories</Strong> are</div>,
|
||||
HERO_HEADER: () => <div>with <Logo src='/icon.svg' /><br />your <Strong>memories</Strong> are</div>,
|
||||
HERO_SLIDE_1_TITLE: 'protected',
|
||||
HERO_SLIDE_1: 'end-to-end encrypted with your password, visible only to you',
|
||||
HERO_SLIDE_2_TITLE: 'synced',
|
||||
|
@ -46,7 +46,7 @@ const englishConstants = {
|
|||
VERIFY_EMAIL: 'verify email',
|
||||
EMAIL_SENT: ({ email }) => (
|
||||
<p>
|
||||
we have sent a mail to <b>{email}</b>
|
||||
we have sent a mail to <b>{email}</b>
|
||||
</p>
|
||||
),
|
||||
CHECK_INBOX: 'please check your inbox (and spam) to complete verification',
|
||||
|
@ -67,10 +67,10 @@ const englishConstants = {
|
|||
'please enter a password that we can use to encrypt your data',
|
||||
PASSPHRASE_DISCLAIMER: () => (
|
||||
<p>
|
||||
we don't store your password, so if you forget,
|
||||
we don't store your password, so if you forget,
|
||||
<strong> we will not be able to help you</strong>
|
||||
{' '}
|
||||
recover your data.
|
||||
recover your data.
|
||||
</p>
|
||||
),
|
||||
PASSPHRASE_HINT: 'password',
|
||||
|
@ -167,6 +167,10 @@ const englishConstants = {
|
|||
SORRY: 'sorry',
|
||||
NO_RECOVERY_KEY_MESSAGE:
|
||||
'due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key',
|
||||
NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE: () => (<>
|
||||
please drop an email to <a href="mailto:support@ente.io">support@ente.io</a> from your registered email
|
||||
</>),
|
||||
CONTACT_SUPPORT: 'contact support',
|
||||
REQUEST_FEATURE: 'request feature',
|
||||
SUPPORT: 'support',
|
||||
CONFIRM: 'confirm',
|
||||
|
@ -182,26 +186,26 @@ const englishConstants = {
|
|||
MESSAGE: 'message',
|
||||
INSTALL_MOBILE_APP: () => (
|
||||
<>
|
||||
install our{' '}
|
||||
install our{' '}
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=io.ente.photos"
|
||||
target="_blank"
|
||||
style={{ color: '#2dc262' }} rel="noreferrer"
|
||||
>
|
||||
android
|
||||
android
|
||||
</a>
|
||||
{' '}
|
||||
or
|
||||
or
|
||||
{' '}
|
||||
<a
|
||||
href="https://apps.apple.com/in/app/ente-photos/id1542026904"
|
||||
style={{ color: '#2dc262' }}
|
||||
target="_blank" rel="noreferrer"
|
||||
>
|
||||
ios app
|
||||
ios app
|
||||
{' '}
|
||||
</a>
|
||||
to automatically backup all your photos
|
||||
to automatically backup all your photos
|
||||
</>
|
||||
),
|
||||
DOWNLOAD_APP_MESSAGE: () => (
|
||||
|
@ -230,41 +234,41 @@ const englishConstants = {
|
|||
FREE_SUBSCRIPTION_INFO: (expiryTime) => (
|
||||
<>
|
||||
<p>
|
||||
you are on the <strong>free</strong>
|
||||
you are on the <strong>free</strong>
|
||||
{' '}
|
||||
plan that expires on{' '}
|
||||
plan that expires on{' '}
|
||||
{dateString(expiryTime)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
RENEWAL_ACTIVE_SUBSCRIPTION_INFO: (expiryTime) => (
|
||||
<p>
|
||||
your subscription will renew on {dateString(expiryTime)}
|
||||
your subscription will renew on {dateString(expiryTime)}
|
||||
</p>
|
||||
),
|
||||
|
||||
RENEWAL_CANCELLED_SUBSCRIPTION_INFO: (expiryTime) => (
|
||||
<>
|
||||
<p>
|
||||
your subscription will be cancelled on {dateString(expiryTime)}
|
||||
your subscription will be cancelled on {dateString(expiryTime)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
|
||||
USAGE_INFO: (usage, quota) => (
|
||||
<p>
|
||||
you have used {usage}
|
||||
you have used {usage}
|
||||
{' '}
|
||||
GB out of your {quota}
|
||||
GB out of your {quota}
|
||||
{' '}
|
||||
GB quota
|
||||
GB quota
|
||||
</p>
|
||||
),
|
||||
|
||||
SUBSCRIPTION_PURCHASE_SUCCESS: (expiryTime) => (
|
||||
<>
|
||||
<p>we've received your payment</p>
|
||||
your subscription is valid till{' '}
|
||||
your subscription is valid till{' '}
|
||||
<strong>{dateString(expiryTime)}</strong>
|
||||
</>
|
||||
),
|
||||
|
@ -287,8 +291,8 @@ const englishConstants = {
|
|||
CANCEL_SUBSCRIPTION_MESSAGE: () => (
|
||||
<>
|
||||
<p>
|
||||
all of your data will be deleted from our servers at the end of
|
||||
this billing period.
|
||||
all of your data will be deleted from our servers at the end of
|
||||
this billing period.
|
||||
</p>
|
||||
<p>are you sure that you want to unsubscribe?</p>
|
||||
</>
|
||||
|
@ -313,8 +317,8 @@ const englishConstants = {
|
|||
<>
|
||||
<p>are you sure you want to delete this album?</p>
|
||||
<p>
|
||||
all files that are present only in this album will be
|
||||
permanently deleted
|
||||
all files that are present only in this album will be
|
||||
permanently deleted
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
|
@ -324,7 +328,7 @@ const englishConstants = {
|
|||
SHAREES: 'shared with',
|
||||
ZERO_SHAREES: () => (
|
||||
<>
|
||||
<p>currently shared with no one 😔</p>
|
||||
<h6>currently shared with no one 😔</h6>
|
||||
<em style={{ color: '#777' }}>"memories are fonder when shared"</em>
|
||||
</>
|
||||
),
|
||||
|
@ -336,42 +340,42 @@ const englishConstants = {
|
|||
SEARCH_HINT: () => <span>try searching for New York, April 14, Christmas...</span>,
|
||||
TERMS_AND_CONDITIONS: () => (
|
||||
<p>
|
||||
I agree to the{' '}
|
||||
I agree to the{' '}
|
||||
<a href="https://ente.io/terms" target="_blank" rel="noreferrer">
|
||||
terms
|
||||
terms
|
||||
</a>
|
||||
{' '}
|
||||
and
|
||||
and
|
||||
{' '}
|
||||
<a href="https://ente.io/privacy" target="_blank" rel="noreferrer">
|
||||
privacy policy
|
||||
privacy policy
|
||||
</a>
|
||||
{' '}
|
||||
</p>
|
||||
),
|
||||
CONFIRM_PASSWORD_NOT_SAVED: () => (
|
||||
<p>
|
||||
i understand that if i lose my password , i may lose my data since
|
||||
my data is{' '}
|
||||
i understand that if i lose my password , i may lose my data since
|
||||
my data is{' '}
|
||||
<a href="https://ente.io/encryption" target="_blank" rel="noreferrer">
|
||||
end-to-end encrypted
|
||||
end-to-end encrypted
|
||||
</a>
|
||||
{' '}
|
||||
with ente
|
||||
with ente
|
||||
</p>
|
||||
),
|
||||
SEARCH_STATS: ({ resultCount, timeTaken }) => (
|
||||
<span>
|
||||
found <span style={{ color: '#2dc262' }}>{resultCount}</span>
|
||||
found <span style={{ color: '#2dc262' }}>{resultCount}</span>
|
||||
{' '}
|
||||
memories (
|
||||
memories (
|
||||
{' '}
|
||||
<span style={{ color: '#2dc262' }}>
|
||||
{' '}
|
||||
{timeTaken}
|
||||
</span>
|
||||
{' '}
|
||||
seconds )
|
||||
seconds )
|
||||
</span>
|
||||
),
|
||||
NOT_FILE_OWNER: 'deleting shared collection files is not allowed',
|
||||
|
@ -397,6 +401,34 @@ const englishConstants = {
|
|||
SHOW_ALL: 'show all',
|
||||
LOGIN_TO_UPLOAD_FILES: (count: number) => count === 1 ? `1 file received. login to upload` : `${count} files received. login to upload`,
|
||||
FILES_TO_BE_UPLOADED: (count: number) => count === 1 ? `1 file received. uploading in a jiffy` : `${count} files received. Uploading in a jiffy`,
|
||||
TWO_FACTOR: 'two-factor',
|
||||
TWO_FACTOR_AUTHENTICATION: 'two-factor authentication',
|
||||
TWO_FACTOR_QR_INSTRUCTION: 'scan the QR code below with your favorite authenticator app',
|
||||
ENTER_CODE_MANUALLY: 'enter the code manually',
|
||||
TWO_FACTOR_MANUAL_CODE_INSTRUCTION: 'please enter this code in your favorite authenticator app',
|
||||
SCAN_QR_CODE: 'scan QR code instead',
|
||||
CONTINUE: 'continue',
|
||||
BACK: 'back',
|
||||
ENABLE_TWO_FACTOR: 'enable two-factor',
|
||||
ENABLE: 'enable',
|
||||
LOST_DEVICE: 'lost two-factor device?',
|
||||
INCORRECT_CODE: 'incorrect code',
|
||||
RECOVER_TWO_FACTOR: 'recover two-factor',
|
||||
TWO_FACTOR_INFO: 'add an additional layer of security by requiring more than your email and password to log in to your account',
|
||||
DISABLE_TWO_FACTOR_HINT: 'disable two-factor authentication',
|
||||
UPDATE_TWO_FACTOR_HINT: 'update your authenticator device',
|
||||
DISABLE: 'disable',
|
||||
RECONFIGURE: 'reconfigure',
|
||||
UPDATE_TWO_FACTOR: 'update two-factor',
|
||||
UPDATE_TWO_FACTOR_MESSAGE: 'continuing forward will void any previously configured authenticators',
|
||||
UPDATE: 'update',
|
||||
DISABLE_TWO_FACTOR: 'disable two-factor',
|
||||
DISABLE_TWO_FACTOR_MESSAGE: 'are you sure you want to disable your two-factor authentication',
|
||||
TWO_FACTOR_SETUP_FAILED: 'failed to setup two factor, please try again',
|
||||
TWO_FACTOR_SETUP_SUCCESS: 'two factor authentication successfully configured',
|
||||
TWO_FACTOR_DISABLE_SUCCESS: 'two factor authentication disabled',
|
||||
TWO_FACTOR_DISABLE_FAILED: 'failed to disable two factor, please try again',
|
||||
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
|
@ -5125,6 +5125,11 @@ react-lifecycles-compat@^3.0.4:
|
|||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-otp-input@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-otp-input/-/react-otp-input-2.3.1.tgz#85d85c01e5d51b0a43c3dc03927292a54d15ed98"
|
||||
integrity sha512-ka14XemxkVh+nl1ykD9HutTyx5ailf4smOZ9gqciQ7GODbepWcjfh1pqsoEB2Mib/sFf1I6OjaPnct5d27+SFA==
|
||||
|
||||
react-overlays@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-5.0.1.tgz#7e2c3cd3c0538048b0b7451d203b1289c561b7f2"
|
||||
|
|
Loading…
Reference in a new issue