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:
parent
7c27e955d5
commit
73c5956ece
25 changed files with 447 additions and 7 deletions
|
@ -0,0 +1,5 @@
|
|||
.dialog-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,5 @@
|
|||
.dialog-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
|
@ -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));
|
20
webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css
Normal file
20
webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css
Normal 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%;
|
||||
}
|
86
webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx
Normal file
86
webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx
Normal 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));
|
|
@ -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';
|
||||
|
|
1
webclient/src/hooks/index.ts
Normal file
1
webclient/src/hooks/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './useReduxEffect';
|
47
webclient/src/hooks/useReduxEffect.tsx
Normal file
47
webclient/src/hooks/useReduxEffect.tsx
Normal 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)
|
||||
}
|
43
webclient/src/store/actions/actionReducer.ts
Normal file
43
webclient/src/store/actions/actionReducer.ts
Normal 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,
|
||||
}
|
||||
}
|
1
webclient/src/store/actions/index.ts
Normal file
1
webclient/src/store/actions/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { actionReducer } from './actionReducer';
|
|
@ -6,6 +6,7 @@ export { SortUtil } from './common';
|
|||
// Server
|
||||
|
||||
export {
|
||||
Types as ServerTypes,
|
||||
Selectors as ServerSelectors,
|
||||
Dispatch as ServerDispatch } from './server';
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -66,5 +66,8 @@ export const Actions = {
|
|||
}),
|
||||
clearLogs: () => ({
|
||||
type: Types.CLEAR_LOGS
|
||||
}),
|
||||
resetPassword: () => ({
|
||||
type: Types.RESET_PASSWORD
|
||||
})
|
||||
}
|
||||
|
|
|
@ -61,5 +61,8 @@ export const Dispatch = {
|
|||
},
|
||||
serverMessage: message => {
|
||||
store.dispatch(Actions.serverMessage(message));
|
||||
},
|
||||
resetPassword: () => {
|
||||
store.dispatch(Actions.resetPassword());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ export class WebClient {
|
|||
port: '',
|
||||
user: '',
|
||||
pass: '',
|
||||
newPassword: '',
|
||||
email: '',
|
||||
clientid: null,
|
||||
reason: null,
|
||||
autojoinrooms: true,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -86,8 +86,7 @@ export class SessionPersistence {
|
|||
}
|
||||
|
||||
static resetPassword() {
|
||||
console.log('Open Modal asking for reset token & new password');
|
||||
|
||||
ServerDispatch.resetPassword();
|
||||
}
|
||||
|
||||
static resetPasswordSuccess() {
|
||||
|
|
|
@ -10,6 +10,8 @@ export interface WebSocketOptions {
|
|||
port: string;
|
||||
user: string;
|
||||
pass: string;
|
||||
newPassword: string;
|
||||
email: string;
|
||||
autojoinrooms: boolean;
|
||||
keepalive: number;
|
||||
clientid: string;
|
||||
|
|
Loading…
Reference in a new issue