diff --git a/webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.css b/webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.css new file mode 100644 index 00000000..731927c1 --- /dev/null +++ b/webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.css @@ -0,0 +1,5 @@ +.dialog-title { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx b/webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx new file mode 100644 index 00000000..9829ef6e --- /dev/null +++ b/webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx @@ -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 ( + + + Request Password Reset + + {handleOnClose ? ( + + + + ) : null} + + + + + + ); +}; + +export default RequestPasswordResetDialog; diff --git a/webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.css b/webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.css new file mode 100644 index 00000000..731927c1 --- /dev/null +++ b/webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.css @@ -0,0 +1,5 @@ +.dialog-title { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.tsx b/webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.tsx new file mode 100644 index 00000000..d0bd8f88 --- /dev/null +++ b/webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.tsx @@ -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 ( + + + Reset Password + + {handleOnClose ? ( + + + + ) : null} + + + + + + ); +}; + +export default ResetPasswordDialog; diff --git a/webclient/src/components/index.ts b/webclient/src/components/index.ts index ea4f4080..d95f6812 100644 --- a/webclient/src/components/index.ts +++ b/webclient/src/components/index.ts @@ -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'; diff --git a/webclient/src/containers/Login/Login.tsx b/webclient/src/containers/Login/Login.tsx index c0901bac..8db2480b 100644 --- a/webclient/src/containers/Login/Login.tsx +++ b/webclient/src/containers/Login/Login.tsx @@ -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 (
{ isConnected && } @@ -138,6 +182,18 @@ const Login = ({ state, description }: LoginProps) => {
+ + + + ); } diff --git a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css new file mode 100644 index 00000000..3087a3bf --- /dev/null +++ b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css @@ -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%; +} diff --git a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx new file mode 100644 index 00000000..0e7386ef --- /dev/null +++ b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx @@ -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 ( +
+
+
+ +
+
+ +
+
+ +
+ ); +}; + +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)); diff --git a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css new file mode 100644 index 00000000..1a512eef --- /dev/null +++ b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css @@ -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%; +} diff --git a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx new file mode 100644 index 00000000..8d7853f3 --- /dev/null +++ b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx @@ -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 ( +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ ); +}; + +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)); diff --git a/webclient/src/forms/index.ts b/webclient/src/forms/index.ts index 141ff1be..8b07668e 100644 --- a/webclient/src/forms/index.ts +++ b/webclient/src/forms/index.ts @@ -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'; diff --git a/webclient/src/hooks/index.ts b/webclient/src/hooks/index.ts new file mode 100644 index 00000000..b00a3bac --- /dev/null +++ b/webclient/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useReduxEffect'; diff --git a/webclient/src/hooks/useReduxEffect.tsx b/webclient/src/hooks/useReduxEffect.tsx new file mode 100644 index 00000000..6d74f620 --- /dev/null +++ b/webclient/src/hooks/useReduxEffect.tsx @@ -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) +} diff --git a/webclient/src/store/actions/actionReducer.ts b/webclient/src/store/actions/actionReducer.ts new file mode 100644 index 00000000..a01dba98 --- /dev/null +++ b/webclient/src/store/actions/actionReducer.ts @@ -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, + } +} diff --git a/webclient/src/store/actions/index.ts b/webclient/src/store/actions/index.ts new file mode 100644 index 00000000..4c5f9ef4 --- /dev/null +++ b/webclient/src/store/actions/index.ts @@ -0,0 +1 @@ +export { actionReducer } from './actionReducer'; diff --git a/webclient/src/store/index.ts b/webclient/src/store/index.ts index b3e26c95..a22bc698 100644 --- a/webclient/src/store/index.ts +++ b/webclient/src/store/index.ts @@ -6,6 +6,7 @@ export { SortUtil } from './common'; // Server export { + Types as ServerTypes, Selectors as ServerSelectors, Dispatch as ServerDispatch } from './server'; diff --git a/webclient/src/store/rootReducer.ts b/webclient/src/store/rootReducer.ts index fac7aee0..0f39d7e3 100644 --- a/webclient/src/store/rootReducer.ts +++ b/webclient/src/store/rootReducer.ts @@ -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 }); diff --git a/webclient/src/store/server/server.actions.ts b/webclient/src/store/server/server.actions.ts index d7499144..3aabc90c 100644 --- a/webclient/src/store/server/server.actions.ts +++ b/webclient/src/store/server/server.actions.ts @@ -66,5 +66,8 @@ export const Actions = { }), clearLogs: () => ({ type: Types.CLEAR_LOGS + }), + resetPassword: () => ({ + type: Types.RESET_PASSWORD }) } diff --git a/webclient/src/store/server/server.dispatch.ts b/webclient/src/store/server/server.dispatch.ts index 1f3a9206..fd54d3d2 100644 --- a/webclient/src/store/server/server.dispatch.ts +++ b/webclient/src/store/server/server.dispatch.ts @@ -61,5 +61,8 @@ export const Dispatch = { }, serverMessage: message => { store.dispatch(Actions.serverMessage(message)); + }, + resetPassword: () => { + store.dispatch(Actions.resetPassword()); } } diff --git a/webclient/src/store/server/server.types.ts b/webclient/src/store/server/server.types.ts index e0d6e0eb..a223f190 100644 --- a/webclient/src/store/server/server.types.ts +++ b/webclient/src/store/server/server.types.ts @@ -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' }; diff --git a/webclient/src/types/forms.ts b/webclient/src/types/forms.ts index 40f7762a..421bc261 100644 --- a/webclient/src/types/forms.ts +++ b/webclient/src/types/forms.ts @@ -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', } diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts index 889bfd69..526b7d31 100644 --- a/webclient/src/websocket/WebClient.ts +++ b/webclient/src/websocket/WebClient.ts @@ -35,6 +35,8 @@ export class WebClient { port: '', user: '', pass: '', + newPassword: '', + email: '', clientid: null, reason: null, autojoinrooms: true, diff --git a/webclient/src/websocket/events/SessionEvents.ts b/webclient/src/websocket/events/SessionEvents.ts index 10cdbf68..a927499f 100644 --- a/webclient/src/websocket/events/SessionEvents.ts +++ b/webclient/src/websocket/events/SessionEvents.ts @@ -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(); diff --git a/webclient/src/websocket/persistence/SessionPersistence.ts b/webclient/src/websocket/persistence/SessionPersistence.ts index d64f1595..90d5fc15 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.ts @@ -86,8 +86,7 @@ export class SessionPersistence { } static resetPassword() { - console.log('Open Modal asking for reset token & new password'); - + ServerDispatch.resetPassword(); } static resetPasswordSuccess() { diff --git a/webclient/src/websocket/services/WebSocketService.ts b/webclient/src/websocket/services/WebSocketService.ts index 594814ca..d46261e8 100644 --- a/webclient/src/websocket/services/WebSocketService.ts +++ b/webclient/src/websocket/services/WebSocketService.ts @@ -10,6 +10,8 @@ export interface WebSocketOptions { port: string; user: string; pass: string; + newPassword: string; + email: string; autojoinrooms: boolean; keepalive: number; clientid: string;