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
This commit is contained in:
Zach H 2021-10-31 22:03:38 -04:00 committed by GitHub
parent 013bb8269f
commit ac300b0b6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 189 additions and 77 deletions

View file

@ -15,12 +15,24 @@ export default class AuthenticationService {
SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT); 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 { static disconnect(): void {
SessionCommands.disconnect(); SessionCommands.disconnect();
} }
static isConnected(state: number): boolean { static isConnected(state: number): boolean {
return state === StatusEnum.LOGGEDIN; return state === StatusEnum.LOGGED_IN;
} }
static isModerator(user: User): boolean { static isModerator(user: User): boolean {

View file

@ -15,7 +15,7 @@ const LoginForm = (props) => {
const { dispatch, handleSubmit } = props; const { dispatch, handleSubmit } = props;
const forgotPassword = () => { const forgotPassword = () => {
console.log('LoginForm.forgotPassword->openForgotPasswordDialog'); console.log("Show recover password dialog, then AuthService.forgotPasswordRequest");
}; };
const onHostChange = ({ host, port }) => { const onHostChange = ({ host, port }) => {

View file

@ -18,6 +18,20 @@ export interface ServerRegisterParams {
realName: string; 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 { export interface AccountActivationParams extends ServerRegisterParams {
activationCode: string; activationCode: string;
clientid: string; clientid: string;

View file

@ -7,30 +7,11 @@ export enum StatusEnum {
DISCONNECTED, DISCONNECTED,
CONNECTING, CONNECTING,
CONNECTED, CONNECTED,
LOGGINGIN, LOGGING_IN,
LOGGEDIN, LOGGED_IN,
REGISTERING,
REGISTERED,
ACTIVATING_ACCOUNT,
ACCOUNT_ACTIVATED,
RECOVERING_PASSWORD,
DISCONNECTING = 99 DISCONNECTING = 99
} }
export enum StatusEnumLabel {
"Disconnected",
"Connecting" ,
"Connected" ,
"Loggingin",
"Loggedin",
"Registering",
"Registered",
"ActivatingAccount",
"AccountActivated",
"RecoveringPassword",
"Disconnecting" = 99
}
export class Host { export class Host {
id?: number; id?: number;
name: string; name: string;
@ -83,14 +64,6 @@ export const KnownHosts = {
[KnownHost.TETRARCH]: { port: 443, host: 'mtg.tetrarch.co/servatrice'}, [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 { export interface Log {
message: string; message: string;
senderId: string; senderId: string;

View file

@ -51,7 +51,6 @@ describe('SessionCommands', () => {
SessionCommands.connect(options, WebSocketConnectReason.REGISTER); SessionCommands.connect(options, WebSocketConnectReason.REGISTER);
expect(SessionCommands.updateStatus).toHaveBeenCalled(); expect(SessionCommands.updateStatus).toHaveBeenCalled();
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.REGISTERING, expect.any(String));
expect(webClient.connect).toHaveBeenCalled(); expect(webClient.connect).toHaveBeenCalled();
expect(webClient.connect).toHaveBeenCalledWith({ ...options, reason: WebSocketConnectReason.REGISTER }); expect(webClient.connect).toHaveBeenCalledWith({ ...options, reason: WebSocketConnectReason.REGISTER });
@ -62,7 +61,6 @@ describe('SessionCommands', () => {
SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT); SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT);
expect(SessionCommands.updateStatus).toHaveBeenCalled(); expect(SessionCommands.updateStatus).toHaveBeenCalled();
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.ACTIVATING_ACCOUNT, expect.any(String));
expect(webClient.connect).toHaveBeenCalled(); expect(webClient.connect).toHaveBeenCalled();
expect(webClient.connect).toHaveBeenCalledWith({ ...options, reason: WebSocketConnectReason.ACTIVATE_ACCOUNT }); expect(webClient.connect).toHaveBeenCalledWith({ ...options, reason: WebSocketConnectReason.ACTIVATE_ACCOUNT });
@ -138,7 +136,7 @@ describe('SessionCommands', () => {
expect(SessionCommands.listUsers).toHaveBeenCalled(); expect(SessionCommands.listUsers).toHaveBeenCalled();
expect(SessionCommands.listRooms).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', () => { it('RespClientUpdateRequired should update status', () => {

View file

@ -4,34 +4,35 @@ import {RoomPersistence, SessionPersistence} from '../persistence';
import webClient from '../WebClient'; import webClient from '../WebClient';
import {guid} from '../utils'; import {guid} from '../utils';
import {WebSocketConnectReason, WebSocketOptions} from "../services/WebSocketService"; import {WebSocketConnectReason, WebSocketOptions} from "../services/WebSocketService";
import {ServerRegisterParams, AccountActivationParams} from "../../store"; import {
AccountActivationParams,
ForgotPasswordChallengeParams,
ForgotPasswordParams,
ForgotPasswordResetParams,
ServerRegisterParams
} from "../../store";
import NormalizeService from "../utils/NormalizeService"; import NormalizeService from "../utils/NormalizeService";
export class SessionCommands { export class SessionCommands {
static connect(options: WebSocketOptions, reason: WebSocketConnectReason): void { static connect(options: WebSocketOptions, reason: WebSocketConnectReason): void {
switch (reason) { switch (reason) {
case WebSocketConnectReason.LOGIN: 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...'); SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
break; 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: default:
console.error('Connection Failed', reason); SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Unknown Connection Attempt: ' + reason);
break; return;
} }
webClient.connect({ ...options, reason }); webClient.connect({ ...options, reason });
} }
static disconnect(): void { static disconnect(): void {
SessionCommands.updateStatus(StatusEnum.DISCONNECTING, 'Disconnecting...');
webClient.disconnect(); webClient.disconnect();
} }
@ -52,20 +53,21 @@ export class SessionCommands {
webClient.protobuf.sendSessionCommand(command, raw => { webClient.protobuf.sendSessionCommand(command, raw => {
const resp = raw['.Response_Login.ext']; 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) { 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: case webClient.protobuf.controller.Response.ResponseCode.RespClientUpdateRequired:
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Login failed: missing features'); SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Login failed: missing features');
break; break;
@ -103,6 +105,8 @@ export class SessionCommands {
default: default:
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Login failed: unknown error: ${raw.responseCode}`); SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Login failed: unknown error: ${raw.responseCode}`);
} }
SessionCommands.disconnect();
}); });
} }
@ -126,14 +130,15 @@ export class SessionCommands {
}); });
webClient.protobuf.sendSessionCommand(sc, raw => { webClient.protobuf.sendSessionCommand(sc, raw => {
if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAccepted) {
SessionCommands.login();
return;
}
let error; let error;
switch (raw.responseCode) { switch (raw.responseCode) {
case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAccepted:
SessionCommands.login();
break;
case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAcceptedNeedsActivation: case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAcceptedNeedsActivation:
SessionCommands.updateStatus(StatusEnum.REGISTERED, "Registration Successful");
SessionPersistence.accountAwaitingActivation(); SessionPersistence.accountAwaitingActivation();
break; break;
case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationDisabled: case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationDisabled:
@ -171,6 +176,8 @@ export class SessionCommands {
if (error) { if (error) {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Registration Failed: ${error}`); SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Registration Failed: ${error}`);
} }
SessionCommands.disconnect();
}); });
}; };
@ -192,14 +199,100 @@ export class SessionCommands {
webClient.protobuf.sendSessionCommand(sc, raw => { webClient.protobuf.sendSessionCommand(sc, raw => {
if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespActivationAccepted) { if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespActivationAccepted) {
SessionCommands.updateStatus(StatusEnum.ACCOUNT_ACTIVATED, 'Account Activation Successful');
SessionCommands.login(); SessionCommands.login();
} else { } else {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed'); SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed');
SessionCommands.disconnect();
SessionPersistence.accountActivationFailed(); 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 { static listUsers(): void {

View file

@ -300,7 +300,7 @@ describe('SessionEvents', () => {
event(data); event(data);
expect(SessionPersistence.updateInfo).toHaveBeenCalledWith(data.serverName, data.serverVersion); 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(); expect(SessionCommands.login).toHaveBeenCalled();
}); });
@ -312,7 +312,6 @@ describe('SessionEvents', () => {
event(data); event(data);
expect(SessionPersistence.updateInfo).toHaveBeenCalledWith(data.serverName, data.serverVersion); expect(SessionPersistence.updateInfo).toHaveBeenCalledWith(data.serverName, data.serverVersion);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.REGISTERING, expect.any(String));
expect(SessionCommands.register).toHaveBeenCalled(); expect(SessionCommands.register).toHaveBeenCalled();
}); });
@ -324,7 +323,6 @@ describe('SessionEvents', () => {
event(data); event(data);
expect(SessionPersistence.updateInfo).toHaveBeenCalledWith(data.serverName, data.serverVersion); expect(SessionPersistence.updateInfo).toHaveBeenCalledWith(data.serverName, data.serverVersion);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.ACTIVATING_ACCOUNT, expect.any(String));
expect(SessionCommands.activateAccount).toHaveBeenCalled(); expect(SessionCommands.activateAccount).toHaveBeenCalled();
}); });

View file

@ -38,7 +38,7 @@ function addToList({ listName, userInfo}: AddToListData) {
} }
function connectionClosed({ reason, reasonStr }: ConnectionClosedData) { function connectionClosed({ reason, reasonStr }: ConnectionClosedData) {
let message = ''; let message;
// @TODO (5) // @TODO (5)
if (reasonStr) { if (reasonStr) {
@ -116,29 +116,34 @@ function serverIdentification(info: ServerIdentificationData) {
const { serverName, serverVersion, protocolVersion } = info; const { serverName, serverVersion, protocolVersion } = info;
if (protocolVersion !== webClient.protocolVersion) { if (protocolVersion !== webClient.protocolVersion) {
SessionCommands.disconnect();
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`); SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`);
SessionCommands.disconnect();
return; return;
} }
switch (webClient.options.reason) { switch (webClient.options.reason) {
case WebSocketConnectReason.LOGIN: case WebSocketConnectReason.LOGIN:
SessionCommands.updateStatus(StatusEnum.LOGGINGIN, 'Logging in...'); SessionCommands.updateStatus(StatusEnum.LOGGING_IN, 'Logging In...');
SessionCommands.login(); SessionCommands.login();
break; break;
case WebSocketConnectReason.REGISTER: case WebSocketConnectReason.REGISTER:
SessionCommands.updateStatus(StatusEnum.REGISTERING, 'Registering...');
SessionCommands.register(); SessionCommands.register();
break; break;
case WebSocketConnectReason.ACTIVATE_ACCOUNT: case WebSocketConnectReason.ACTIVATE_ACCOUNT:
SessionCommands.updateStatus(StatusEnum.ACTIVATING_ACCOUNT, 'Activating account...');
SessionCommands.activateAccount(); SessionCommands.activateAccount();
break; break;
case WebSocketConnectReason.RECOVER_PASSWORD: case WebSocketConnectReason.PASSWORD_RESET_REQUEST:
console.log('ServerIdentificationData.recoverPassword'); SessionCommands.resetPasswordRequest();
break;
case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE:
SessionCommands.resetPasswordChallenge();
break;
case WebSocketConnectReason.PASSWORD_RESET:
SessionCommands.resetPassword();
break; break;
default: default:
console.error("Undefined type", webClient.options.reason); SessionCommands.updateStatus(StatusEnum.DISCONNECTED, "Unknown Connection Reason: " + webClient.options.reason);
SessionCommands.disconnect();
break; break;
} }

View file

@ -80,4 +80,21 @@ export class SessionPersistence {
static accountActivationFailed() { static accountActivationFailed() {
console.log("Account activation failed, show an action here"); 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");
}
} }

View file

@ -20,7 +20,9 @@ export enum WebSocketConnectReason {
LOGIN, LOGIN,
REGISTER, REGISTER,
ACTIVATE_ACCOUNT, ACTIVATE_ACCOUNT,
RECOVER_PASSWORD, PASSWORD_RESET_REQUEST,
PASSWORD_RESET_CHALLENGE,
PASSWORD_RESET
} }
export class WebSocketService { export class WebSocketService {