Dev/jchamish/forgotpassword (#4481)

* Implementation of Forgotten Password Reset

* Update webclient/src/hooks/useReduxEffect.tsx

Co-authored-by: Zach H <zahalpern+github@gmail.com>
This commit is contained in:
Joseph Chamish 2021-11-19 21:00:05 -05:00 committed by GitHub
parent 7c27e955d5
commit 73c5956ece
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 447 additions and 7 deletions

View file

@ -0,0 +1,5 @@
.dialog-title {
display: flex;
justify-content: space-between;
align-items: center;
}

View file

@ -0,0 +1,36 @@
import React from 'react';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography';
import { RequestPasswordResetForm } from 'forms';
import './RequestPasswordResetDialog.css';
const RequestPasswordResetDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => {
const handleOnClose = () => {
handleClose();
}
return (
<Dialog onClose={handleOnClose} open={isOpen}>
<DialogTitle disableTypography className="dialog-title">
<Typography variant="h6">Request Password Reset</Typography>
{handleOnClose ? (
<IconButton onClick={handleOnClose}>
<CloseIcon />
</IconButton>
) : null}
</DialogTitle>
<DialogContent>
<RequestPasswordResetForm onSubmit={onSubmit}></RequestPasswordResetForm>
</DialogContent>
</Dialog>
);
};
export default RequestPasswordResetDialog;

View file

@ -0,0 +1,5 @@
.dialog-title {
display: flex;
justify-content: space-between;
align-items: center;
}

View file

@ -0,0 +1,36 @@
import React from 'react';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography';
import { ResetPasswordForm } from 'forms';
import './ResetPasswordDialog.css';
const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => {
const handleOnClose = () => {
handleClose();
}
return (
<Dialog onClose={handleOnClose} open={isOpen}>
<DialogTitle disableTypography className="dialog-title">
<Typography variant="h6">Reset Password</Typography>
{handleOnClose ? (
<IconButton onClick={handleOnClose}>
<CloseIcon />
</IconButton>
) : null}
</DialogTitle>
<DialogContent>
<ResetPasswordForm onSubmit={onSubmit}></ResetPasswordForm>
</DialogContent>
</Dialog>
);
};
export default ResetPasswordDialog;

View file

@ -20,3 +20,5 @@ export { default as ModGuard } from './Guard/ModGuard';
// Dialogs
export { default as CardImportDialog } from './CardImportDialog/CardImportDialog';
export { default as RequestPasswordResetDialog } from './RequestPasswordResetDialog/RequestPasswordResetDialog';
export { default as ResetPasswordDialog } from './ResetPasswordDialog/ResetPasswordDialog';

View file

@ -1,5 +1,5 @@
// eslint-disable-next-line
import React from "react";
import React, { useState } from "react";
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { makeStyles } from '@material-ui/core/styles';
@ -7,11 +7,14 @@ import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import { AuthenticationService } from 'api';
import { RequestPasswordResetDialog, ResetPasswordDialog } from 'components';
import { LoginForm } from 'forms';
import { useReduxEffect } from 'hooks';
import { Images } from 'images';
import { RouteEnum } from 'types';
import { ServerSelectors } from 'store';
import { ServerSelectors, ServerTypes } from 'store';
import './Login.css';
@ -56,6 +59,15 @@ const Login = ({ state, description }: LoginProps) => {
const classes = useStyles();
const isConnected = AuthenticationService.isConnected(state);
const [dialogState, setDialogState] = useState({
openRequest: false,
openReset: false
});
useReduxEffect(() => {
openResetPasswordDialog();
}, ServerTypes.RESET_PASSWORD, []);
const showDescription = () => {
return !isConnected && description?.length;
};
@ -64,6 +76,38 @@ const Login = ({ state, description }: LoginProps) => {
console.log('Login.createAccount->openForgotPasswordDialog');
};
const handleRequestPasswordResetDialogSubmit = async ({ user, email, host, port }) => {
if (email) {
AuthenticationService.resetPasswordChallenge({ user, email, host, port } as any);
} else {
AuthenticationService.resetPasswordRequest({ user, host, port } as any);
}
closeRequestPasswordResetDialog();
};
const handleResetPasswordDialogSubmit = async ({ user, token, newPassword, passwordAgain, host, port }) => {
AuthenticationService.resetPassword({ user, token, newPassword, host, port } as any);
closeResetPasswordDialog();
};
const closeRequestPasswordResetDialog = () => {
setDialogState(s => ({ ...s, openRequest: false }));
}
const openRequestPasswordResetDialog = () => {
setDialogState(s => ({ ...s, openRequest: true }));
}
const closeResetPasswordDialog = () => {
setDialogState(s => ({ ...s, openReset: false }));
}
const openResetPasswordDialog = () => {
setDialogState(s => ({ ...s, openReset: true }));
}
return (
<div className={'login overflow-scroll ' + classes.root}>
{ isConnected && <Redirect from="*" to={RouteEnum.SERVER} />}
@ -138,6 +182,18 @@ const Login = ({ state, description }: LoginProps) => {
</div>
</Paper>
</div>
<RequestPasswordResetDialog
isOpen={dialogState.openRequest}
onSubmit={handleRequestPasswordResetDialogSubmit}
handleClose={closeRequestPasswordResetDialog}
/>
<ResetPasswordDialog
isOpen={dialogState.openReset}
onSubmit={handleResetPasswordDialogSubmit}
handleClose={closeResetPasswordDialog}
/>
</div>
);
}

View file

@ -0,0 +1,20 @@
.RequestPasswordResetForm {
width: 100%;
}
.RequestPasswordResetForm-item {
margin-bottom: 20px;
}
.RequestPasswordResetForm-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: -20px;
margin-bottom: 20px;
font-weight: bold;
}
.RequestPasswordResetForm-submit {
width: 100%;
}

View file

@ -0,0 +1,66 @@
// eslint-disable-next-line
import React from "react";
import { connect } from 'react-redux';
import { Form, Field, reduxForm, change } from 'redux-form'
import Button from '@material-ui/core/Button';
import { InputField, KnownHosts } from 'components';
import { FormKey } from 'types';
import './RequestPasswordResetForm.css';
const RequestPasswordResetForm = (props) => {
const { dispatch, handleSubmit } = props;
const onHostChange = ({ host, port }) => {
dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'host', host));
dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'port', port));
}
return (
<Form className="RequestPasswordResetForm" onSubmit={handleSubmit}>
<div className="RequestPasswordResetForm-items">
<div className="RequestPasswordResetForm-item">
<Field label="Username" name="user" component={InputField} autoComplete="username" />
</div>
<div className="RequestPasswordResetForm-item">
<KnownHosts onChange={onHostChange} />
</div>
</div>
<Button className="RequestPasswordResetForm-submit rounded tall" color="primary" variant="contained" type="submit">
Request Reset Token
</Button>
</Form>
);
};
const propsMap = {
form: FormKey.RESET_PASSWORD_REQUEST,
validate: values => {
const errors: any = {};
if (!values.user) {
errors.user = 'Required';
}
if (!values.host) {
errors.host = 'Required';
}
if (!values.port) {
errors.port = 'Required';
}
return errors;
}
};
const mapStateToProps = () => ({
initialValues: {
// host: "mtg.tetrarch.co/servatrice",
// port: "443"
// host: "server.cockatrice.us",
// port: "4748"
}
});
export default connect(mapStateToProps)(reduxForm(propsMap)(RequestPasswordResetForm));

View file

@ -0,0 +1,20 @@
.ResetPasswordForm {
width: 100%;
}
.ResetPasswordForm-item {
margin-bottom: 20px;
}
.ResetPasswordForm-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: -20px;
margin-bottom: 20px;
font-weight: bold;
}
.ResetPasswordForm-submit {
width: 100%;
}

View file

@ -0,0 +1,86 @@
// eslint-disable-next-line
import React from "react";
import { connect } from 'react-redux';
import { Form, Field, reduxForm, change } from 'redux-form'
import Button from '@material-ui/core/Button';
import { InputField, KnownHosts } from 'components';
import { FormKey } from 'types';
import './ResetPasswordForm.css';
const ResetPasswordForm = (props) => {
const { dispatch, handleSubmit } = props;
const onHostChange = ({ host, port }) => {
dispatch(change(FormKey.RESET_PASSWORD, 'host', host));
dispatch(change(FormKey.RESET_PASSWORD, 'port', port));
}
return (
<Form className="ResetPasswordForm" onSubmit={handleSubmit}>
<div className="ResetPasswordForm-items">
<div className="ResetPasswordForm-item">
<Field label="Username" name="user" component={InputField} autoComplete="username" />
</div>
<div className="ResetPasswordForm-item">
<Field label="Token" name="token" component={InputField} />
</div>
<div className="ResetPasswordForm-item">
<Field label="Password" name="newPassword" component={InputField} />
</div>
<div className="ResetPasswordForm-item">
<Field label="Password Again" name="passwordAgain" component={InputField} />
</div>
<div className="ResetPasswordForm-item">
<KnownHosts onChange={onHostChange} />
</div>
</div>
<Button className="ResetPasswordForm-submit rounded tall" color="primary" variant="contained" type="submit">
Change Password
</Button>
</Form>
);
};
const propsMap = {
form: FormKey.RESET_PASSWORD,
validate: values => {
const errors: any = {};
if (!values.user) {
errors.user = 'Required';
}
if (!values.token) {
errors.token = 'Required';
}
if (!values.newPassword) {
errors.newPassword = 'Required';
}
if (!values.passwordAgain) {
errors.passwordAgain = 'Required';
} else if (values.newPassword !== values.passwordAgain) {
errors.passwordAgain = 'Passwords don\'t match'
}
if (!values.host) {
errors.host = 'Required';
}
if (!values.port) {
errors.port = 'Required';
}
return errors;
}
};
const mapStateToProps = () => ({
initialValues: {
// host: "mtg.tetrarch.co/servatrice",
// port: "443"
// host: "server.cockatrice.us",
// port: "4748"
}
});
export default connect(mapStateToProps)(reduxForm(propsMap)(ResetPasswordForm));

View file

@ -3,3 +3,5 @@ export { default as ConnectForm } from './ConnectForm/ConnectForm';
export { default as LoginForm } from './LoginForm/LoginForm';
export { default as RegisterForm } from './RegisterForm/RegisterForm';
export { default as SearchForm } from './SearchForm/SearchForm';
export { default as RequestPasswordResetForm } from './RequestPasswordResetForm/RequestPasswordResetForm';
export { default as ResetPasswordForm } from './ResetPasswordForm/ResetPasswordForm';

View file

@ -0,0 +1 @@
export * from './useReduxEffect';

View file

@ -0,0 +1,47 @@
/**
File is adapted from https://github.com/Qeepsake/use-redux-effect under MIT License
* @author Aspect Apps Limited
* @description
*/
import { useRef, useEffect, DependencyList } from 'react'
import { useStore } from 'react-redux'
import { castArray } from 'lodash'
import { AnyAction } from 'redux'
export type ReduxEffect = (action: AnyAction) => void
/**
* Subscribes to redux store events
*
* @param effect
* @param type
* @param deps
*/
export function useReduxEffect(
effect: ReduxEffect,
type: string | string[],
deps: DependencyList = [],
): void {
const currentValue = useRef(null)
const store = useStore()
const handleChange = (): void => {
const state = store.getState()
const action = state.action
const previousValue = currentValue.current
currentValue.current = action.count
if (
previousValue !== action.count &&
castArray(type).includes(action.type)
) {
effect(action)
}
}
useEffect(() => {
const unsubscribe = store.subscribe(handleChange)
return (): void => unsubscribe()
}, deps)
}

View file

@ -0,0 +1,43 @@
/**
* @author Luke Brandon Farrell
* @description Application reducer.
*/
import { AnyAction } from 'redux'
interface InitialState {
type: string | null
payload: any
meta: any
error: boolean
count: number
}
/**
* Initial data.
*/
const initialState: InitialState = {
type: null,
payload: null,
meta: null,
error: false,
count: 0,
}
/**
* Calculates the application state.
*
* @param state
* @param action
* @return {*}
*/
export const actionReducer = (
state = initialState,
action: AnyAction,
): InitialState => {
return {
...state,
...action,
count: state.count + 1,
}
}

View file

@ -0,0 +1 @@
export { actionReducer } from './actionReducer';

View file

@ -6,6 +6,7 @@ export { SortUtil } from './common';
// Server
export {
Types as ServerTypes,
Selectors as ServerSelectors,
Dispatch as ServerDispatch } from './server';

View file

@ -3,10 +3,12 @@ import { combineReducers } from 'redux';
import { roomsReducer } from './rooms';
import { serverReducer } from './server';
import { reducer as formReducer } from 'redux-form'
import { actionReducer } from './actions'
export default combineReducers({
rooms: roomsReducer,
server: serverReducer,
form: formReducer
form: formReducer,
action: actionReducer
});

View file

@ -66,5 +66,8 @@ export const Actions = {
}),
clearLogs: () => ({
type: Types.CLEAR_LOGS
}),
resetPassword: () => ({
type: Types.RESET_PASSWORD
})
}

View file

@ -61,5 +61,8 @@ export const Dispatch = {
},
serverMessage: message => {
store.dispatch(Actions.serverMessage(message));
},
resetPassword: () => {
store.dispatch(Actions.resetPassword());
}
}

View file

@ -15,5 +15,6 @@ export const Types = {
USER_JOINED: '[Server] User Joined',
USER_LEFT: '[Server] User Left',
VIEW_LOGS: '[Server] View Logs',
CLEAR_LOGS: '[Server] Clear Logs'
CLEAR_LOGS: '[Server] Clear Logs',
RESET_PASSWORD: '[Server] Reset Password'
};

View file

@ -4,6 +4,8 @@ export enum FormKey {
CARD_IMPORT = 'CARD_IMPORT',
CONNECT = 'CONNECT',
LOGIN = 'LOGIN',
RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST',
RESET_PASSWORD = 'RESET_PASSWORD',
REGISTER = 'REGISTER',
SEARCH_LOGS = 'SEARCH_LOGS',
}

View file

@ -35,6 +35,8 @@ export class WebClient {
port: '',
user: '',
pass: '',
newPassword: '',
email: '',
clientid: null,
reason: null,
autojoinrooms: true,

View file

@ -114,7 +114,6 @@ function removeFromList({ listName, userName }: RemoveFromListData) {
function serverIdentification(info: ServerIdentificationData) {
const { serverName, serverVersion, protocolVersion, serverOptions } = info;
if (protocolVersion !== webClient.protocolVersion) {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`);
SessionCommands.disconnect();

View file

@ -86,8 +86,7 @@ export class SessionPersistence {
}
static resetPassword() {
console.log('Open Modal asking for reset token & new password');
ServerDispatch.resetPassword();
}
static resetPasswordSuccess() {

View file

@ -10,6 +10,8 @@ export interface WebSocketOptions {
port: string;
user: string;
pass: string;
newPassword: string;
email: string;
autojoinrooms: boolean;
keepalive: number;
clientid: string;