Webatrice: fix login bugs (#4557)

* fix login after failed connection attempts, limit connection attempt time

* fix register hashed password and salt

* add feature detection and Unsupported Browser screen

* nit

Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
Jeremy Letto 2022-02-04 13:07:15 -06:00 committed by GitHub
parent 81d031ca0f
commit bb16ae09ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 104 additions and 18 deletions

View file

@ -6,6 +6,7 @@ import CssBaseline from '@material-ui/core/CssBaseline';
import { store } from 'store'; import { store } from 'store';
import { Header } from 'components'; import { Header } from 'components';
import Routes from './AppShellRoutes'; import Routes from './AppShellRoutes';
import FeatureDetection from './FeatureDetection';
import './AppShell.css'; import './AppShell.css';
@ -26,6 +27,8 @@ class AppShell extends Component {
<div className="AppShell" onContextMenu={this.handleContextMenu}> <div className="AppShell" onContextMenu={this.handleContextMenu}>
<Router> <Router>
<Header /> <Header />
<FeatureDetection />
<Routes /> <Routes />
</Router> </Router>
</div> </div>

View file

@ -10,7 +10,8 @@ import {
Room, Room,
Server, Server,
Login, Login,
Logs Logs,
Unsupported
} from 'containers'; } from 'containers';
const Routes = () => ( const Routes = () => (
@ -24,6 +25,7 @@ const Routes = () => (
{<Route path={RouteEnum.ROOM} render={() => <Room />} />} {<Route path={RouteEnum.ROOM} render={() => <Room />} />}
<Route path={RouteEnum.SERVER} render={() => <Server />} /> <Route path={RouteEnum.SERVER} render={() => <Server />} />
<Route path={RouteEnum.LOGIN} render={() => <Login />} /> <Route path={RouteEnum.LOGIN} render={() => <Login />} />
<Route path={RouteEnum.UNSUPPORTED} render={() => <Unsupported />} />
<Redirect from="*" to={RouteEnum.LOGIN} /> <Redirect from="*" to={RouteEnum.LOGIN} />
</Switch> </Switch>

View file

@ -0,0 +1,26 @@
import { useEffect, useState } from 'react';
import { Redirect } from 'react-router-dom';
import { dexieService } from 'services';
import { RouteEnum } from 'types';
const FeatureDetection = () => {
const [unsupported, setUnsupported] = useState(false);
useEffect(() => {
const features: Promise<any>[] = [
detectIndexedDB(),
];
Promise.all(features).catch((e) => setUnsupported(true));
}, []);
return unsupported
? <Redirect from="*" to={RouteEnum.UNSUPPORTED} />
: <></>;
function detectIndexedDB() {
return dexieService.testConnection();
}
};
export default FeatureDetection;

View file

@ -88,7 +88,7 @@ const Login = ({ state, description }: LoginProps) => {
useReduxEffect(() => { useReduxEffect(() => {
resetSubmitButton(); resetSubmitButton();
}, [ServerTypes.LOGIN_FAILED], []); }, [ServerTypes.CONNECTION_FAILED, ServerTypes.LOGIN_FAILED], []);
useReduxEffect(({ options: { hashedPassword } }) => { useReduxEffect(({ options: { hashedPassword } }) => {
if (hostIdToRemember) { if (hostIdToRemember) {

View file

@ -0,0 +1,18 @@
.Unsupported {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.Unsupported-paper {
width: 600px;
max-width: 100%;
padding: 40px;
text-align: center;
}
.Unsupported-paper__header {
margin-bottom: 40px;
}

View file

@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import './Unsupported.css';
const Unsupported = () => {
return (
<div className='Unsupported'>
<Paper className='Unsupported-paper'>
<div className='Unsupported-paper__header'>
<Typography variant="h1">Unsupported Browser</Typography>
<Typography variant="subtitle1">Please update your browser and/or check your permissions.</Typography>
</div>
<Typography variant="subtitle2">Note: Private browsing causes some browsers to disable certain permissions or features.</Typography>
</Paper>
</div>
);
};
const mapStateToProps = state => ({
});
export default withRouter(connect(mapStateToProps)(Unsupported));

View file

@ -7,3 +7,4 @@ export { default as Player } from './Player/Player';
export { default as Server } from './Server/Server'; export { default as Server } from './Server/Server';
export { default as Logs } from './Logs/Logs'; export { default as Logs } from './Logs/Logs';
export { default as Login } from './Login/Login'; export { default as Login } from './Login/Login';
export { default as Unsupported } from './Unsupported/Unsupported';

View file

@ -28,6 +28,10 @@ class DexieService {
get hosts() { get hosts() {
return this.db.table(Stores.HOSTS); return this.db.table(Stores.HOSTS);
} }
testConnection() {
return this.db.open();
}
} }
export const dexieService = new DexieService(); export const dexieService = new DexieService();

View file

@ -1 +1,2 @@
export * from './DexieDTOs'; export * from './DexieDTOs';
export * from './DexieService';

View file

@ -18,6 +18,9 @@ export const Actions = {
type: Types.CONNECTION_CLOSED, type: Types.CONNECTION_CLOSED,
reason reason
}), }),
connectionFailed: () => ({
type: Types.CONNECTION_FAILED,
}),
serverMessage: message => ({ serverMessage: message => ({
type: Types.SERVER_MESSAGE, type: Types.SERVER_MESSAGE,
message message

View file

@ -15,6 +15,9 @@ export const Dispatch = {
connectionClosed: reason => { connectionClosed: reason => {
store.dispatch(Actions.connectionClosed(reason)); store.dispatch(Actions.connectionClosed(reason));
}, },
connectionFailed: () => {
store.dispatch(Actions.connectionFailed());
},
updateBuddyList: buddyList => { updateBuddyList: buddyList => {
store.dispatch(Actions.updateBuddyList(buddyList)); store.dispatch(Actions.updateBuddyList(buddyList));
}, },

View file

@ -3,6 +3,7 @@ export const Types = {
LOGIN_SUCCESSFUL: '[Server] Login Successful', LOGIN_SUCCESSFUL: '[Server] Login Successful',
LOGIN_FAILED: '[Server] Login Failed', LOGIN_FAILED: '[Server] Login Failed',
CONNECTION_CLOSED: '[Server] Connection Closed', CONNECTION_CLOSED: '[Server] Connection Closed',
CONNECTION_FAILED: '[Server] Connection Failed',
SERVER_MESSAGE: '[Server] Server Message', SERVER_MESSAGE: '[Server] Server Message',
UPDATE_BUDDY_LIST: '[Server] Update Buddy List', UPDATE_BUDDY_LIST: '[Server] Update Buddy List',
ADD_TO_BUDDY_LIST: '[Server] Add to Buddy List', ADD_TO_BUDDY_LIST: '[Server] Add to Buddy List',

View file

@ -10,4 +10,5 @@ export enum RouteEnum {
ACCOUNT = '/account', ACCOUNT = '/account',
ADMINISTRATION = '/administration', ADMINISTRATION = '/administration',
REPLAYS = '/replays', REPLAYS = '/replays',
UNSUPPORTED = '/unsupported',
} }

View file

@ -141,11 +141,6 @@ export class SessionCommands {
const passwordSalt = raw['.Response_PasswordSalt.ext']?.passwordSalt; const passwordSalt = raw['.Response_PasswordSalt.ext']?.passwordSalt;
switch (webClient.options.reason) { switch (webClient.options.reason) {
case WebSocketConnectReason.REGISTER: {
SessionCommands.register(passwordSalt);
break;
}
case WebSocketConnectReason.ACTIVATE_ACCOUNT: { case WebSocketConnectReason.ACTIVATE_ACCOUNT: {
SessionCommands.activateAccount(passwordSalt); SessionCommands.activateAccount(passwordSalt);
break; break;
@ -174,11 +169,6 @@ export class SessionCommands {
} }
switch (webClient.options.reason) { switch (webClient.options.reason) {
case WebSocketConnectReason.REGISTER: {
SessionPersistence.registrationFailed('Failed to retrieve password salt');
break;
}
case WebSocketConnectReason.ACTIVATE_ACCOUNT: { case WebSocketConnectReason.ACTIVATE_ACCOUNT: {
SessionPersistence.accountActivationFailed(); SessionPersistence.accountActivationFailed();
break; break;

View file

@ -3,7 +3,7 @@ import { Room, StatusEnum, User, WebSocketConnectReason } from 'types';
import { SessionCommands } from '../commands'; import { SessionCommands } from '../commands';
import { RoomPersistence, SessionPersistence } from '../persistence'; import { RoomPersistence, SessionPersistence } from '../persistence';
import { ProtobufEvents } from '../services/ProtobufService'; import { ProtobufEvents } from '../services/ProtobufService';
import { passwordSaltSupported } from '../utils'; import { generateSalt, passwordSaltSupported } from '../utils';
import webClient from '../WebClient'; import webClient from '../WebClient';
export const SessionEvents: ProtobufEvents = { export const SessionEvents: ProtobufEvents = {
@ -130,11 +130,8 @@ function serverIdentification(info: ServerIdentificationData) {
} }
break; break;
case WebSocketConnectReason.REGISTER: case WebSocketConnectReason.REGISTER:
if (passwordSaltSupported(serverOptions, webClient)) { const passwordSalt = passwordSaltSupported(serverOptions, webClient) ? generateSalt() : null;
SessionCommands.requestPasswordSalt(); SessionCommands.register(passwordSalt);
} else {
SessionCommands.register();
}
break; break;
case WebSocketConnectReason.ACTIVATE_ACCOUNT: case WebSocketConnectReason.ACTIVATE_ACCOUNT:
if (passwordSaltSupported(serverOptions, webClient)) { if (passwordSaltSupported(serverOptions, webClient)) {

View file

@ -21,6 +21,10 @@ export class SessionPersistence {
ServerDispatch.connectionClosed(reason); ServerDispatch.connectionClosed(reason);
} }
static connectionFailed() {
ServerDispatch.connectionFailed();
}
static updateBuddyList(buddyList) { static updateBuddyList(buddyList) {
ServerDispatch.updateBuddyList(buddyList); ServerDispatch.updateBuddyList(buddyList);
} }

View file

@ -4,6 +4,7 @@ import { ServerStatus, StatusEnum, WebSocketConnectOptions } from 'types';
import { KeepAliveService } from './KeepAliveService'; import { KeepAliveService } from './KeepAliveService';
import { WebClient } from '../WebClient'; import { WebClient } from '../WebClient';
import { SessionPersistence } from '../persistence';
export class WebSocketService { export class WebSocketService {
private socket: WebSocket; private socket: WebSocket;
@ -60,7 +61,10 @@ export class WebSocketService {
const socket = new WebSocket(url); const socket = new WebSocket(url);
socket.binaryType = 'arraybuffer'; socket.binaryType = 'arraybuffer';
const connectionTimer = setTimeout(() => socket.close(), this.keepalive);
socket.onopen = () => { socket.onopen = () => {
clearTimeout(connectionTimer);
this.updateStatus(StatusEnum.CONNECTED, 'Connected'); this.updateStatus(StatusEnum.CONNECTED, 'Connected');
this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: Function) => { this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: Function) => {
@ -79,6 +83,7 @@ export class WebSocketService {
socket.onerror = () => { socket.onerror = () => {
this.updateStatus(StatusEnum.DISCONNECTED, 'Connection Failed'); this.updateStatus(StatusEnum.DISCONNECTED, 'Connection Failed');
SessionPersistence.connectionFailed();
}; };
socket.onmessage = (event: MessageEvent) => { socket.onmessage = (event: MessageEvent) => {