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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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;