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);
}
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 {

View file

@ -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 }) => {

View file

@ -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;

View file

@ -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;

View file

@ -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', () => {

View file

@ -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 {

View file

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

View file

@ -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;
}

View file

@ -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");
}
}

View file

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