1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 12:56:41 +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 { TestDialog } from '~/containers/dialogs/TestDialog';
import { ProfileDialog } from '~/containers/dialogs/ProfileDialog'; import { ProfileDialog } from '~/containers/dialogs/ProfileDialog';
import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog'; import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog';
import { RestorePasswordDialog } from '~/containers/dialogs/RestorePasswordDialog';
import { DIALOGS } from '~/redux/modal/constants'; import { DIALOGS } from '~/redux/modal/constants';
export const DIALOG_CONTENT = { export const DIALOG_CONTENT = {
@ -20,6 +21,7 @@ export const DIALOG_CONTENT = {
[DIALOGS.TEST]: TestDialog, [DIALOGS.TEST]: TestDialog,
[DIALOGS.PROFILE]: ProfileDialog, [DIALOGS.PROFILE]: ProfileDialog,
[DIALOGS.RESTORE_REQUEST]: RestoreRequestDialog, [DIALOGS.RESTORE_REQUEST]: RestoreRequestDialog,
[DIALOGS.RESTORE_PASSWORD]: RestorePasswordDialog,
}; };
export const NODE_EDITOR_DIALOGS = { export const NODE_EDITOR_DIALOGS = {

View file

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

View file

@ -25,8 +25,6 @@ const mapDispatchToProps = {
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & IDialogProps & {}; type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & IDialogProps & {};
console.log('initial', MODAL_ACTIONS);
const LoginDialogUnconnected: FC<IProps> = ({ const LoginDialogUnconnected: FC<IProps> = ({
onRequestClose, onRequestClose,
error, 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 * as AUTH_ACTIONS from '~/redux/auth/actions';
import pick from 'ramda/es/pick'; import pick from 'ramda/es/pick';
import { selectAuthRestore } from '~/redux/auth/selectors'; import { selectAuthRestore } from '~/redux/auth/selectors';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { ERROR_LITERAL } from '~/constants/errors'; import { ERROR_LITERAL } from '~/constants/errors';
import { Icon } from '~/components/input/Icon'; import { Icon } from '~/components/input/Icon';
import { useCloseOnEscape } from '~/utils/hooks';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
restore: selectAuthRestore(state), restore: selectAuthRestore(state),
@ -28,7 +28,7 @@ const RestoreRequestDialogUnconnected: FC<IProps> = ({
onRequestClose, onRequestClose,
authRequestRestoreCode, authRequestRestoreCode,
}) => { }) => {
const [field, setField] = useState(); const [field, setField] = useState('');
const onSubmit = useCallback( const onSubmit = useCallback(
event => { event => {
@ -76,6 +76,8 @@ const RestoreRequestDialogUnconnected: FC<IProps> = ({
[is_succesfull] [is_succesfull]
); );
useCloseOnEscape(onRequestClose);
return ( return (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<BetterScrollDialog <BetterScrollDialog

View file

@ -21,7 +21,6 @@ render(
/* /*
[Stage 0]: [Stage 0]:
- illustrate restoreRequestDialog
- check if email is registered at social login - check if email is registered at social login
- friendship - friendship
- password restore - password restore
@ -31,6 +30,10 @@ render(
- mobile header - mobile header
- profile cover upload - profile cover upload
- illustrate restoreRequestDialog
- illustrate 404
- illustrate login
[stage 1] [stage 1]
- import videos - import videos
- import graffiti - import graffiti

View file

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

View file

@ -72,3 +72,9 @@ export const apiRequestRestoreCode = ({ field }): Promise<IResultWithStatus<{}>>
.post(API.USER.REQUEST_CODE(), { field }) .post(API.USER.REQUEST_CODE(), { field })
.then(resultMiddleware) .then(resultMiddleware)
.catch(errorMiddleware); .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', SET_RESTORE: 'SET_RESTORE',
REQUEST_RESTORE_CODE: 'REQUEST_RESTORE_CODE', REQUEST_RESTORE_CODE: 'REQUEST_RESTORE_CODE',
RESTORE_PASSWORD: 'RESTORE_PASSWORD', SHOW_RESTORE_MODAL: 'SHOW_RESTORE_MODAL',
}; };
export const USER_ERRORS = { export const USER_ERRORS = {

View file

@ -14,7 +14,7 @@ import {
authLoggedIn, authLoggedIn,
authSetLastSeenMessages, authSetLastSeenMessages,
authPatchUser, authPatchUser,
authRestorePassword, authShowRestoreModal,
authSetRestore, authSetRestore,
authRequestRestoreCode, authRequestRestoreCode,
} from '~/redux/auth/actions'; } from '~/redux/auth/actions';
@ -27,6 +27,7 @@ import {
apiAuthGetUpdates, apiAuthGetUpdates,
apiUpdateUser, apiUpdateUser,
apiRequestRestoreCode, apiRequestRestoreCode,
apiCheckRestoreCode,
} from '~/redux/auth/api'; } from '~/redux/auth/api';
import { modalSetShown, modalShowDialog } from '~/redux/modal/actions'; import { modalSetShown, modalShowDialog } from '~/redux/modal/actions';
import { selectToken, selectAuthProfile, selectAuthUser, selectAuthUpdates } from './selectors'; 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 })); 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) { if (!code && !code.length) {
return yield put( return yield put(
authSetRestore({ 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() { function* authSaga() {
@ -313,7 +327,7 @@ function* authSaga() {
yield takeLatest(AUTH_USER_ACTIONS.SEND_MESSAGE, sendMessage); yield takeLatest(AUTH_USER_ACTIONS.SEND_MESSAGE, sendMessage);
yield takeLatest(AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES, setLastSeenMessages); yield takeLatest(AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES, setLastSeenMessages);
yield takeLatest(AUTH_USER_ACTIONS.PATCH_USER, patchUser); 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); yield takeLatest(AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE, requestRestoreCode);
} }

View file

@ -1,14 +1,4 @@
import { ValueOf } from '~/redux/types'; 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 = { export const DIALOGS = {
EDITOR_IMAGE: 'EDITOR_IMAGE', EDITOR_IMAGE: 'EDITOR_IMAGE',
@ -19,6 +9,7 @@ export const DIALOGS = {
LOADING: 'LOADING', LOADING: 'LOADING',
PROFILE: 'PROFILE', PROFILE: 'PROFILE',
RESTORE_REQUEST: 'RESTORE_REQUEST', RESTORE_REQUEST: 'RESTORE_REQUEST',
RESTORE_PASSWORD: 'RESTORE_PASSWORD',
TEST: 'TEST', TEST: 'TEST',
}; };

View file

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

View file

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