1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46:40 +07:00

password restore dialog

This commit is contained in:
Fedor Katurov 2019-11-25 16:52:01 +07:00
parent 0cfc6357b9
commit 078c531e93
14 changed files with 270 additions and 33 deletions

View file

@ -8,6 +8,7 @@ import { LoadingDialog } from '~/containers/dialogs/LoadingDialog';
import { TestDialog } from '~/containers/dialogs/TestDialog';
import { ProfileDialog } from '~/containers/dialogs/ProfileDialog';
import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog';
import { RestorePasswordDialog } from '~/containers/dialogs/RestorePasswordDialog';
import { DIALOGS } from '~/redux/modal/constants';
export const DIALOG_CONTENT = {
@ -20,6 +21,7 @@ export const DIALOG_CONTENT = {
[DIALOGS.TEST]: TestDialog,
[DIALOGS.PROFILE]: ProfileDialog,
[DIALOGS.RESTORE_REQUEST]: RestoreRequestDialog,
[DIALOGS.RESTORE_PASSWORD]: RestorePasswordDialog,
};
export const NODE_EDITOR_DIALOGS = {

View file

@ -15,6 +15,7 @@ export const ERRORS = {
USER_EXIST: 'User_Exist',
INCORRECT_PASSWORD: 'Incorrect_Password',
CODE_IS_INVALID: 'Code_Is_Invalid',
DOESNT_MATCH: 'Doesnt_Match',
};
export const ERROR_LITERAL = {
@ -34,4 +35,5 @@ export const ERROR_LITERAL = {
[ERRORS.USER_EXIST]: 'Такой пользователь уже существует',
[ERRORS.INCORRECT_PASSWORD]: 'Неправильный пароль',
[ERRORS.CODE_IS_INVALID]: 'Код не существует или устарел',
[ERRORS.DOESNT_MATCH]: 'Пароли не совпадают',
};

View file

@ -25,8 +25,6 @@ const mapDispatchToProps = {
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & IDialogProps & {};
console.log('initial', MODAL_ACTIONS);
const LoginDialogUnconnected: FC<IProps> = ({
onRequestClose,
error,

View file

@ -0,0 +1,160 @@
import React, { FC, useState, useMemo, useCallback, useEffect } from 'react';
import { IDialogProps } from '~/redux/types';
import { connect } from 'react-redux';
import { BetterScrollDialog } from '../BetterScrollDialog';
import { Group } from '~/components/containers/Group';
import { InputText } from '~/components/input/InputText';
import { Button } from '~/components/input/Button';
import styles from './styles.scss';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import pick from 'ramda/es/pick';
import { selectAuthRestore } from '~/redux/auth/selectors';
import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
import { Icon } from '~/components/input/Icon';
import { useCloseOnEscape } from '~/utils/hooks';
const mapStateToProps = state => ({
restore: selectAuthRestore(state),
});
const mapDispatchToProps = pick(['authRequestRestoreCode', 'authSetRestore'], AUTH_ACTIONS);
type IProps = IDialogProps & ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const RestorePasswordDialogUnconnected: FC<IProps> = ({
restore: { error, is_loading, is_succesfull, user },
authSetRestore,
onRequestClose,
authRequestRestoreCode,
}) => {
const [password, setPassword] = useState('');
const [password_again, setPasswordAgain] = useState('');
const doesnt_match = useMemo(
() => !password || !password_again || password.trim() !== password_again.trim(),
[password_again, password]
);
const onSubmit = useCallback(
event => {
event.preventDefault();
if (doesnt_match) return;
// if (!field) return;
//
// authRequestRestoreCode(field);
},
[doesnt_match]
);
useEffect(() => {
if (error || is_succesfull) {
authSetRestore({ error: null, is_succesfull: false });
}
}, [password, password_again]);
const buttons = useMemo(
() => (
<Group className={styles.buttons}>
<Button color={doesnt_match ? 'outline' : 'primary'}>Восстановить</Button>
</Group>
),
[doesnt_match]
);
const overlay = useMemo(
() =>
is_succesfull ? (
<Group className={styles.shade}>
<Icon icon="check" size={64} />
<div>Отлично, добро пожаловать домой!</div>
<div />
<Button color="secondary" onClick={onRequestClose}>
Ура!
</Button>
</Group>
) : null,
[is_succesfull]
);
const not_ready = useMemo(() => (is_loading && !user ? <div className={styles.shade} /> : null), [
is_loading,
user,
]);
const invalid_code = useMemo(
() =>
!is_loading && !user ? (
<Group className={styles.error_shade}>
<Icon icon="close" size={64} />
<div>{ERROR_LITERAL[error || ERRORS.CODE_IS_INVALID]}</div>
<div className={styles.spacer} />
<Button color="primary" onClick={onRequestClose}>
Очень жаль
</Button>
</Group>
) : null,
[is_loading, user, error]
);
useCloseOnEscape(onRequestClose);
return (
<form onSubmit={onSubmit}>
<BetterScrollDialog
footer={buttons}
width={300}
onClose={onRequestClose}
is_loading={is_loading}
error={error && ERROR_LITERAL[error]}
overlay={overlay || not_ready || invalid_code}
>
<div className={styles.wrap}>
<Group>
<div className={styles.header}>
Пришло время сменить пароль{user && user.username && `, ~${user.username}`}
</div>
<InputText
title="Новый пароль"
value={password}
handler={setPassword}
autoFocus
type="password"
/>
<InputText
title="Ещё раз"
type="password"
value={password_again}
handler={setPasswordAgain}
error={password_again && doesnt_match && ERROR_LITERAL[ERRORS.DOESNT_MATCH]}
/>
<Group className={styles.text}>
<p>Новый пароль должен быть не короче 6 символов.</p>
<p>
Вряд ли кто-нибудь будет пытаться нас взломать, но сложный пароль всегда лучше
простого.
</p>
</Group>
</Group>
</div>
</BetterScrollDialog>
</form>
);
};
const RestorePasswordDialog = connect(
mapStateToProps,
mapDispatchToProps
)(RestorePasswordDialogUnconnected);
export { RestorePasswordDialog };

View file

@ -0,0 +1,59 @@
.wrap {
padding: $gap $gap $gap * 4;
}
.buttons {
padding: $gap;
}
.text {
font: $font_14_regular;
padding: $gap;
color: darken(white, 50%);
}
.shade,
.error_shade {
@include outer_shadow();
background: $content_bg;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius;
padding: $gap * 2;
box-sizing: border-box;
text-transform: uppercase;
font: $font_18_semibold;
text-align: center;
color: $wisegreen;
svg {
fill: $wisegreen;
}
}
.error_shade {
color: $red;
svg {
fill: $red;
}
}
.header {
font: $font_18_semibold;
text-transform: uppercase;
padding: $gap;
padding-bottom: $gap * 2;
}
.spacer {
height: $gap * 4;
}

View file

@ -10,9 +10,9 @@ import styles from './styles.scss';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import pick from 'ramda/es/pick';
import { selectAuthRestore } from '~/redux/auth/selectors';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { ERROR_LITERAL } from '~/constants/errors';
import { Icon } from '~/components/input/Icon';
import { useCloseOnEscape } from '~/utils/hooks';
const mapStateToProps = state => ({
restore: selectAuthRestore(state),
@ -28,7 +28,7 @@ const RestoreRequestDialogUnconnected: FC<IProps> = ({
onRequestClose,
authRequestRestoreCode,
}) => {
const [field, setField] = useState();
const [field, setField] = useState('');
const onSubmit = useCallback(
event => {
@ -76,6 +76,8 @@ const RestoreRequestDialogUnconnected: FC<IProps> = ({
[is_succesfull]
);
useCloseOnEscape(onRequestClose);
return (
<form onSubmit={onSubmit}>
<BetterScrollDialog

View file

@ -21,15 +21,18 @@ render(
/*
[Stage 0]:
- illustrate restoreRequestDialog
- check if email is registered at social login
- friendship
- password restore
- signup?
- cover change
- sticky header
- mobile header
- profile cover upload
- check if email is registered at social login
- friendship
- password restore
- signup?
- cover change
- sticky header
- mobile header
- profile cover upload
- illustrate restoreRequestDialog
- illustrate 404
- illustrate login
[stage 1]
- import videos

View file

@ -87,7 +87,7 @@ export const authSetRestore = (restore: Partial<IAuthState['restore']>) => ({
restore,
});
export const authRestorePassword = (code: string) => ({
type: AUTH_USER_ACTIONS.RESTORE_PASSWORD,
export const authShowRestoreModal = (code: string) => ({
type: AUTH_USER_ACTIONS.SHOW_RESTORE_MODAL,
code,
});

View file

@ -72,3 +72,9 @@ export const apiRequestRestoreCode = ({ field }): Promise<IResultWithStatus<{}>>
.post(API.USER.REQUEST_CODE(), { field })
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiCheckRestoreCode = ({ code }): Promise<IResultWithStatus<{}>> =>
api
.get(API.USER.REQUEST_CODE(code))
.then(resultMiddleware)
.catch(errorMiddleware);

View file

@ -21,7 +21,7 @@ export const AUTH_USER_ACTIONS = {
SET_RESTORE: 'SET_RESTORE',
REQUEST_RESTORE_CODE: 'REQUEST_RESTORE_CODE',
RESTORE_PASSWORD: 'RESTORE_PASSWORD',
SHOW_RESTORE_MODAL: 'SHOW_RESTORE_MODAL',
};
export const USER_ERRORS = {

View file

@ -14,7 +14,7 @@ import {
authLoggedIn,
authSetLastSeenMessages,
authPatchUser,
authRestorePassword,
authShowRestoreModal,
authSetRestore,
authRequestRestoreCode,
} from '~/redux/auth/actions';
@ -27,6 +27,7 @@ import {
apiAuthGetUpdates,
apiUpdateUser,
apiRequestRestoreCode,
apiCheckRestoreCode,
} from '~/redux/auth/api';
import { modalSetShown, modalShowDialog } from '~/redux/modal/actions';
import { selectToken, selectAuthProfile, selectAuthUser, selectAuthUpdates } from './selectors';
@ -290,7 +291,7 @@ function* requestRestoreCode({ field }: ReturnType<typeof authRequestRestoreCode
yield put(authSetRestore({ is_loading: false, is_succesfull: true }));
}
function* restorePassword({ code }: ReturnType<typeof authRestorePassword>) {
function* showRestoreModal({ code }: ReturnType<typeof authShowRestoreModal>) {
if (!code && !code.length) {
return yield put(
authSetRestore({
@ -299,6 +300,19 @@ function* restorePassword({ code }: ReturnType<typeof authRestorePassword>) {
})
);
}
yield put(authSetRestore({ user: null, is_loading: true }));
const { error, data } = yield call(apiCheckRestoreCode, { code });
if (data.error || error || !data.user) {
return yield put(
authSetRestore({ is_loading: false, error: data.error || error || ERRORS.CODE_IS_INVALID })
);
}
yield put(modalShowDialog(DIALOGS.RESTORE_PASSWORD));
yield put(authSetRestore({ user: data.user, is_loading: false }));
}
function* authSaga() {
@ -313,7 +327,7 @@ function* authSaga() {
yield takeLatest(AUTH_USER_ACTIONS.SEND_MESSAGE, sendMessage);
yield takeLatest(AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES, setLastSeenMessages);
yield takeLatest(AUTH_USER_ACTIONS.PATCH_USER, patchUser);
yield takeLatest(AUTH_USER_ACTIONS.RESTORE_PASSWORD, restorePassword);
yield takeLatest(AUTH_USER_ACTIONS.SHOW_RESTORE_MODAL, showRestoreModal);
yield takeLatest(AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE, requestRestoreCode);
}

View file

@ -1,14 +1,4 @@
import { ValueOf } from '~/redux/types';
import { LoginDialog } from '~/containers/dialogs/LoginDialog';
import { LoadingDialog } from '~/containers/dialogs/LoadingDialog';
import { EditorDialogImage } from '~/containers/editors/EditorDialogImage';
import { EditorDialogText } from '~/containers/editors/EditorDialogText';
import { EditorDialogVideo } from '~/containers/editors/EditorDialogVideo';
import { EditorDialogAudio } from '~/containers/editors/EditorDialogAudio';
import { NODE_TYPES } from '../node/constants';
import { TestDialog } from '~/containers/dialogs/TestDialog';
import { ProfileDialog } from '~/containers/dialogs/ProfileDialog';
import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog';
export const DIALOGS = {
EDITOR_IMAGE: 'EDITOR_IMAGE',
@ -19,6 +9,7 @@ export const DIALOGS = {
LOADING: 'LOADING',
PROFILE: 'PROFILE',
RESTORE_REQUEST: 'RESTORE_REQUEST',
RESTORE_PASSWORD: 'RESTORE_PASSWORD',
TEST: 'TEST',
};

View file

@ -9,8 +9,8 @@ export interface IModalState {
}
const INITIAL_STATE: IModalState = {
is_shown: true,
dialog: DIALOGS.RESTORE_REQUEST,
is_shown: false,
dialog: null,
};
export default createReducer(INITIAL_STATE, MODAL_HANDLERS);

View file

@ -1,6 +1,6 @@
import { takeEvery, put } from 'redux-saga/effects';
import { LocationChangeAction, LOCATION_CHANGE } from 'connected-react-router';
import { authOpenProfile, authRestorePassword } from '../auth/actions';
import { authOpenProfile, authShowRestoreModal } from '../auth/actions';
function* onPathChange({
payload: {
@ -14,7 +14,7 @@ function* onPathChange({
if (pathname.match(/^\/restore\/([\w\-]+)/)) {
const [, code] = pathname.match(/^\/restore\/([\w\-]+)/);
return yield put(authRestorePassword(code));
return yield put(authShowRestoreModal(code));
}
}