From ac300b0b6d3c3566cd923b8e73c59691d7241299 Mon Sep 17 00:00:00 2001 From: Zach H Date: Sun, 31 Oct 2021 22:03:38 -0400 Subject: [PATCH] Support password reset workflow on Webatrice (#4445) * Support password reset workflow. Also fix issue where a user would be disconnected "randomly" if they had a failed login, then successful one. Refactored a bit on Status Labels since they weren't really necessary and added complexity. * Disconnect in default cases where we don't know what to do, but shouldn't stay connected to the server --- webclient/src/api/AuthenticationService.tsx | 14 +- webclient/src/forms/LoginForm/LoginForm.tsx | 2 +- .../src/store/server/server.interfaces.ts | 14 ++ webclient/src/types/server.tsx | 31 +--- .../commands/SessionCommands.spec.ts | 4 +- .../src/websocket/commands/SessionCommands.ts | 155 ++++++++++++++---- .../websocket/events/SessionEvents.spec.ts | 4 +- .../src/websocket/events/SessionEvents.ts | 21 ++- .../persistence/SessionPersistence.ts | 17 ++ .../websocket/services/WebSocketService.ts | 4 +- 10 files changed, 189 insertions(+), 77 deletions(-) diff --git a/webclient/src/api/AuthenticationService.tsx b/webclient/src/api/AuthenticationService.tsx index f53c3546..ec0290ed 100644 --- a/webclient/src/api/AuthenticationService.tsx +++ b/webclient/src/api/AuthenticationService.tsx @@ -15,12 +15,24 @@ export default class AuthenticationService { SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT); } + static resetPasswordRequest(options: WebSocketOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_REQUEST); + } + + static resetPasswordChallenge(options: WebSocketOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE); + } + + static resetPassword(options: WebSocketOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET); + } + static disconnect(): void { SessionCommands.disconnect(); } static isConnected(state: number): boolean { - return state === StatusEnum.LOGGEDIN; + return state === StatusEnum.LOGGED_IN; } static isModerator(user: User): boolean { diff --git a/webclient/src/forms/LoginForm/LoginForm.tsx b/webclient/src/forms/LoginForm/LoginForm.tsx index 0ce75366..c32cd5b0 100644 --- a/webclient/src/forms/LoginForm/LoginForm.tsx +++ b/webclient/src/forms/LoginForm/LoginForm.tsx @@ -15,7 +15,7 @@ const LoginForm = (props) => { const { dispatch, handleSubmit } = props; const forgotPassword = () => { - console.log('LoginForm.forgotPassword->openForgotPasswordDialog'); + console.log("Show recover password dialog, then AuthService.forgotPasswordRequest"); }; const onHostChange = ({ host, port }) => { diff --git a/webclient/src/store/server/server.interfaces.ts b/webclient/src/store/server/server.interfaces.ts index c86952b2..b08c6f57 100644 --- a/webclient/src/store/server/server.interfaces.ts +++ b/webclient/src/store/server/server.interfaces.ts @@ -18,6 +18,20 @@ export interface ServerRegisterParams { realName: string; } +export interface ForgotPasswordParams { + user: string; + clientid: string; +} + +export interface ForgotPasswordChallengeParams extends ForgotPasswordParams { + email: string; +} + +export interface ForgotPasswordResetParams extends ForgotPasswordParams { + token: string; + newPassword: string; +} + export interface AccountActivationParams extends ServerRegisterParams { activationCode: string; clientid: string; diff --git a/webclient/src/types/server.tsx b/webclient/src/types/server.tsx index d3a15881..4b4adce6 100644 --- a/webclient/src/types/server.tsx +++ b/webclient/src/types/server.tsx @@ -7,30 +7,11 @@ export enum StatusEnum { DISCONNECTED, CONNECTING, CONNECTED, - LOGGINGIN, - LOGGEDIN, - REGISTERING, - REGISTERED, - ACTIVATING_ACCOUNT, - ACCOUNT_ACTIVATED, - RECOVERING_PASSWORD, + LOGGING_IN, + LOGGED_IN, DISCONNECTING = 99 } -export enum StatusEnumLabel { - "Disconnected", - "Connecting" , - "Connected" , - "Loggingin", - "Loggedin", - "Registering", - "Registered", - "ActivatingAccount", - "AccountActivated", - "RecoveringPassword", - "Disconnecting" = 99 -} - export class Host { id?: number; name: string; @@ -83,14 +64,6 @@ export const KnownHosts = { [KnownHost.TETRARCH]: { port: 443, host: 'mtg.tetrarch.co/servatrice'}, } -export const getStatusEnumLabel = (statusEnum: number) => { - if (StatusEnumLabel[statusEnum] !== undefined) { - return StatusEnumLabel[statusEnum]; - } - - return "Unknown"; -}; - export interface Log { message: string; senderId: string; diff --git a/webclient/src/websocket/commands/SessionCommands.spec.ts b/webclient/src/websocket/commands/SessionCommands.spec.ts index 39755b2d..6fdd3cf4 100644 --- a/webclient/src/websocket/commands/SessionCommands.spec.ts +++ b/webclient/src/websocket/commands/SessionCommands.spec.ts @@ -51,7 +51,6 @@ describe('SessionCommands', () => { SessionCommands.connect(options, WebSocketConnectReason.REGISTER); expect(SessionCommands.updateStatus).toHaveBeenCalled(); - expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.REGISTERING, expect.any(String)); expect(webClient.connect).toHaveBeenCalled(); expect(webClient.connect).toHaveBeenCalledWith({ ...options, reason: WebSocketConnectReason.REGISTER }); @@ -62,7 +61,6 @@ describe('SessionCommands', () => { SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT); expect(SessionCommands.updateStatus).toHaveBeenCalled(); - expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.ACTIVATING_ACCOUNT, expect.any(String)); expect(webClient.connect).toHaveBeenCalled(); expect(webClient.connect).toHaveBeenCalledWith({ ...options, reason: WebSocketConnectReason.ACTIVATE_ACCOUNT }); @@ -138,7 +136,7 @@ describe('SessionCommands', () => { expect(SessionCommands.listUsers).toHaveBeenCalled(); expect(SessionCommands.listRooms).toHaveBeenCalled(); - expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGEDIN, 'Logged in.'); + expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGED_IN, 'Logged in.'); }); it('RespClientUpdateRequired should update status', () => { diff --git a/webclient/src/websocket/commands/SessionCommands.ts b/webclient/src/websocket/commands/SessionCommands.ts index 3b61b30c..1d255072 100644 --- a/webclient/src/websocket/commands/SessionCommands.ts +++ b/webclient/src/websocket/commands/SessionCommands.ts @@ -4,34 +4,35 @@ import {RoomPersistence, SessionPersistence} from '../persistence'; import webClient from '../WebClient'; import {guid} from '../utils'; import {WebSocketConnectReason, WebSocketOptions} from "../services/WebSocketService"; -import {ServerRegisterParams, AccountActivationParams} from "../../store"; +import { + AccountActivationParams, + ForgotPasswordChallengeParams, + ForgotPasswordParams, + ForgotPasswordResetParams, + ServerRegisterParams +} from "../../store"; import NormalizeService from "../utils/NormalizeService"; export class SessionCommands { static connect(options: WebSocketOptions, reason: WebSocketConnectReason): void { switch (reason) { case WebSocketConnectReason.LOGIN: + case WebSocketConnectReason.REGISTER: + case WebSocketConnectReason.ACTIVATE_ACCOUNT: + case WebSocketConnectReason.PASSWORD_RESET_REQUEST: + case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: + case WebSocketConnectReason.PASSWORD_RESET: SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); break; - case WebSocketConnectReason.REGISTER: - SessionCommands.updateStatus(StatusEnum.REGISTERING, 'Registering...'); - break; - case WebSocketConnectReason.ACTIVATE_ACCOUNT: - SessionCommands.updateStatus(StatusEnum.ACTIVATING_ACCOUNT, 'Activating Account...'); - break; - case WebSocketConnectReason.RECOVER_PASSWORD: - SessionCommands.updateStatus(StatusEnum.RECOVERING_PASSWORD, 'Recovering Password...'); - break; default: - console.error('Connection Failed', reason); - break; + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Unknown Connection Attempt: ' + reason); + return; } webClient.connect({ ...options, reason }); } static disconnect(): void { - SessionCommands.updateStatus(StatusEnum.DISCONNECTING, 'Disconnecting...'); webClient.disconnect(); } @@ -52,20 +53,21 @@ export class SessionCommands { webClient.protobuf.sendSessionCommand(command, raw => { const resp = raw['.Response_Login.ext']; + if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { + const { buddyList, ignoreList, userInfo } = resp; + + SessionPersistence.updateBuddyList(buddyList); + SessionPersistence.updateIgnoreList(ignoreList); + SessionPersistence.updateUser(userInfo); + + SessionCommands.listUsers(); + SessionCommands.listRooms(); + + SessionCommands.updateStatus(StatusEnum.LOGGED_IN, 'Logged in.'); + return; + } + switch(raw.responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - const { buddyList, ignoreList, userInfo } = resp; - - SessionPersistence.updateBuddyList(buddyList); - SessionPersistence.updateIgnoreList(ignoreList); - SessionPersistence.updateUser(userInfo); - - SessionCommands.listUsers(); - SessionCommands.listRooms(); - - SessionCommands.updateStatus(StatusEnum.LOGGEDIN, 'Logged in.'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespClientUpdateRequired: SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Login failed: missing features'); break; @@ -103,6 +105,8 @@ export class SessionCommands { default: SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Login failed: unknown error: ${raw.responseCode}`); } + + SessionCommands.disconnect(); }); } @@ -126,14 +130,15 @@ export class SessionCommands { }); webClient.protobuf.sendSessionCommand(sc, raw => { + if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAccepted) { + SessionCommands.login(); + return; + } + let error; switch (raw.responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAccepted: - SessionCommands.login(); - break; case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAcceptedNeedsActivation: - SessionCommands.updateStatus(StatusEnum.REGISTERED, "Registration Successful"); SessionPersistence.accountAwaitingActivation(); break; case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationDisabled: @@ -171,6 +176,8 @@ export class SessionCommands { if (error) { SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Registration Failed: ${error}`); } + + SessionCommands.disconnect(); }); }; @@ -192,14 +199,100 @@ export class SessionCommands { webClient.protobuf.sendSessionCommand(sc, raw => { if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespActivationAccepted) { - SessionCommands.updateStatus(StatusEnum.ACCOUNT_ACTIVATED, 'Account Activation Successful'); SessionCommands.login(); } else { SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed'); + SessionCommands.disconnect(); SessionPersistence.accountActivationFailed(); } }); + } + static resetPasswordRequest(): void { + const options = webClient.options as unknown as ForgotPasswordParams; + + const forgotPasswordConfig = { + ...webClient.clientConfig, + userName: options.user, + clientid: options.clientid + }; + + const CmdForgotPasswordRequest = webClient.protobuf.controller.Command_ForgotPasswordRequest.create(forgotPasswordConfig); + + const sc = webClient.protobuf.controller.SessionCommand.create({ + '.Command_ForgotPasswordRequest.ext' : CmdForgotPasswordRequest + }); + + webClient.protobuf.sendSessionCommand(sc, raw => { + if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { + const resp = raw[".Response_ForgotPasswordRequest.ext"]; + + if (resp.challengeEmail) { + SessionPersistence.resetPasswordChallenge(); + } else { + SessionPersistence.resetPassword(); + } + } else { + SessionPersistence.resetPasswordFailed(); + } + + SessionCommands.disconnect(); + }); + } + + static resetPasswordChallenge(): void { + const options = webClient.options as unknown as ForgotPasswordChallengeParams; + + const forgotPasswordChallengeConfig = { + ...webClient.clientConfig, + userName: options.user, + clientid: options.clientid, + email: options.email + }; + + const CmdForgotPasswordChallenge = webClient.protobuf.controller.Command_ForgotPasswordChallenge.create(forgotPasswordChallengeConfig); + + const sc = webClient.protobuf.controller.SessionCommand.create({ + '.Command_ForgotPasswordChallenge.ext' : CmdForgotPasswordChallenge + }); + + webClient.protobuf.sendSessionCommand(sc, raw => { + if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { + SessionPersistence.resetPassword(); + } else { + SessionPersistence.resetPasswordFailed(); + } + + SessionCommands.disconnect(); + }); + } + + static resetPassword(): void { + const options = webClient.options as unknown as ForgotPasswordResetParams; + + const forgotPasswordResetConfig = { + ...webClient.clientConfig, + userName: options.user, + clientid: options.clientid, + token: options.token, + newPassword: options.newPassword + }; + + const CmdForgotPasswordReset = webClient.protobuf.controller.Command_ForgotPasswordReset.create(forgotPasswordResetConfig); + + const sc = webClient.protobuf.controller.SessionCommand.create({ + '.Command_ForgotPasswordReset.ext' : CmdForgotPasswordReset + }); + + webClient.protobuf.sendSessionCommand(sc, raw => { + if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { + SessionPersistence.resetPasswordSuccess(); + } else { + SessionPersistence.resetPasswordFailed(); + } + + SessionCommands.disconnect(); + }); } static listUsers(): void { diff --git a/webclient/src/websocket/events/SessionEvents.spec.ts b/webclient/src/websocket/events/SessionEvents.spec.ts index fa3f1a83..02416b93 100644 --- a/webclient/src/websocket/events/SessionEvents.spec.ts +++ b/webclient/src/websocket/events/SessionEvents.spec.ts @@ -300,7 +300,7 @@ describe('SessionEvents', () => { event(data); expect(SessionPersistence.updateInfo).toHaveBeenCalledWith(data.serverName, data.serverVersion); - expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGINGIN, expect.any(String)); + expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGING_IN, expect.any(String)); expect(SessionCommands.login).toHaveBeenCalled(); }); @@ -312,7 +312,6 @@ describe('SessionEvents', () => { event(data); expect(SessionPersistence.updateInfo).toHaveBeenCalledWith(data.serverName, data.serverVersion); - expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.REGISTERING, expect.any(String)); expect(SessionCommands.register).toHaveBeenCalled(); }); @@ -324,7 +323,6 @@ describe('SessionEvents', () => { event(data); expect(SessionPersistence.updateInfo).toHaveBeenCalledWith(data.serverName, data.serverVersion); - expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.ACTIVATING_ACCOUNT, expect.any(String)); expect(SessionCommands.activateAccount).toHaveBeenCalled(); }); diff --git a/webclient/src/websocket/events/SessionEvents.ts b/webclient/src/websocket/events/SessionEvents.ts index 97a24565..8e4cbc22 100644 --- a/webclient/src/websocket/events/SessionEvents.ts +++ b/webclient/src/websocket/events/SessionEvents.ts @@ -38,7 +38,7 @@ function addToList({ listName, userInfo}: AddToListData) { } function connectionClosed({ reason, reasonStr }: ConnectionClosedData) { - let message = ''; + let message; // @TODO (5) if (reasonStr) { @@ -116,29 +116,34 @@ function serverIdentification(info: ServerIdentificationData) { const { serverName, serverVersion, protocolVersion } = info; if (protocolVersion !== webClient.protocolVersion) { - SessionCommands.disconnect(); SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`); + SessionCommands.disconnect(); return; } switch (webClient.options.reason) { case WebSocketConnectReason.LOGIN: - SessionCommands.updateStatus(StatusEnum.LOGGINGIN, 'Logging in...'); + SessionCommands.updateStatus(StatusEnum.LOGGING_IN, 'Logging In...'); SessionCommands.login(); break; case WebSocketConnectReason.REGISTER: - SessionCommands.updateStatus(StatusEnum.REGISTERING, 'Registering...'); SessionCommands.register(); break; case WebSocketConnectReason.ACTIVATE_ACCOUNT: - SessionCommands.updateStatus(StatusEnum.ACTIVATING_ACCOUNT, 'Activating account...'); SessionCommands.activateAccount(); break; - case WebSocketConnectReason.RECOVER_PASSWORD: - console.log('ServerIdentificationData.recoverPassword'); + case WebSocketConnectReason.PASSWORD_RESET_REQUEST: + SessionCommands.resetPasswordRequest(); + break; + case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: + SessionCommands.resetPasswordChallenge(); + break; + case WebSocketConnectReason.PASSWORD_RESET: + SessionCommands.resetPassword(); break; default: - console.error("Undefined type", webClient.options.reason); + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, "Unknown Connection Reason: " + webClient.options.reason); + SessionCommands.disconnect(); break; } diff --git a/webclient/src/websocket/persistence/SessionPersistence.ts b/webclient/src/websocket/persistence/SessionPersistence.ts index 3b9ea3de..13ba5414 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.ts @@ -80,4 +80,21 @@ export class SessionPersistence { static accountActivationFailed() { console.log("Account activation failed, show an action here"); } + + static resetPasswordChallenge() { + console.log("Open Modal asking for Email address associated with account"); + } + + static resetPassword() { + console.log("Open Modal asking for reset token & new password"); + + } + + static resetPasswordSuccess() { + console.log("User password successfully changed Alert!"); + } + + static resetPasswordFailed() { + console.log("Open Alert telling user their password request failed for some reason"); + } } diff --git a/webclient/src/websocket/services/WebSocketService.ts b/webclient/src/websocket/services/WebSocketService.ts index b4c1e2de..b0e52e11 100644 --- a/webclient/src/websocket/services/WebSocketService.ts +++ b/webclient/src/websocket/services/WebSocketService.ts @@ -20,7 +20,9 @@ export enum WebSocketConnectReason { LOGIN, REGISTER, ACTIVATE_ACCOUNT, - RECOVER_PASSWORD, + PASSWORD_RESET_REQUEST, + PASSWORD_RESET_CHALLENGE, + PASSWORD_RESET } export class WebSocketService {