From 0c1f0b1f76a06fd431572c8549963f8c149a039f Mon Sep 17 00:00:00 2001 From: Mario Reder Date: Fri, 27 Jul 2018 21:35:53 +0200 Subject: [PATCH] feature: Handle authentication message is related to #57 --- src/main/Connection.ts | 29 ++++++++-- src/main/Connector.ts | 8 +++ src/models/Message.model.ts | 2 + src/models/State.model.ts | 2 + src/renderer/Connector.ts | 27 +++++++++- src/renderer/actions/connection.ts | 22 ++++++++ .../actions/models/connection.model.ts | 14 +++++ .../components/areas/ConnectionArea.tsx | 32 +++++------ .../components/areas/SendPasswordArea.tsx | 53 +++++++++++++++++-- .../components/buttons/SMMButton.scss | 6 +++ src/renderer/components/buttons/SMMButton.tsx | 11 ++-- src/renderer/components/views/AppView.tsx | 1 - src/renderer/reducers/connection.ts | 9 ++++ src/renderer/reducers/index.ts | 2 + 14 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 src/renderer/components/buttons/SMMButton.scss diff --git a/src/main/Connection.ts b/src/main/Connection.ts index ecccfce..e653589 100644 --- a/src/main/Connection.ts +++ b/src/main/Connection.ts @@ -18,7 +18,8 @@ import { ConnectionDenied, IServerClient, IServerMessage, - IConnectionDenied + IConnectionDenied, + Authentication } from '../../proto/ServerClientMessage' const UPDATE_INTERVAL = 32 @@ -28,8 +29,6 @@ const UPDATE_INTERVAL = 32 * Net64+ server. */ export class Connection { - // public server: Server - private ws: WS private playerId?: number @@ -270,6 +269,9 @@ export class Connection { case ServerMessage.MessageType.PLAYER_REORDER: this.onPlayerReorder(serverMessage) break + case ServerMessage.MessageType.AUTHENTICATION: + this.onAuthentication(serverMessage) + break } } @@ -348,6 +350,27 @@ export class Connection { emulator!.setPlayerId(playerId) } + /** + * Handle server authentication message. + * + * @param {IServerMessage} serverMessage - The decoded message + */ + private onAuthentication (serverMessage: IServerMessage): void { + const authentication = serverMessage.authentication + if (!authentication) return + const { status, throttle } = authentication + if (status == null) return + switch (status) { + case Authentication.Status.ACCEPTED: + connector.acceptAuthentication() + break + case Authentication.Status.DENIED: + if (throttle == null) return + connector.denyAuthentication(throttle) + break + } + } + /** * Handle player list update message. * diff --git a/src/main/Connector.ts b/src/main/Connector.ts index e5fd571..56b9163 100644 --- a/src/main/Connector.ts +++ b/src/main/Connector.ts @@ -110,6 +110,14 @@ export class Connector { this.window.webContents.send(MainMessage.WRONG_VERSION, { majorVersion, minorVersion }) } + public acceptAuthentication (): void { + this.window.webContents.send(MainMessage.AUTHENTICATION_ACCEPTED) + } + + public denyAuthentication (throttle: number): void { + this.window.webContents.send(MainMessage.AUTHENTICATION_DENIED, throttle) + } + public globalChatMessage (message: string, senderId: number): void { this.window.webContents.send(MainMessage.CHAT_GLOBAL, { message, senderId }) } diff --git a/src/models/Message.model.ts b/src/models/Message.model.ts index 8ebf589..ccd3485 100644 --- a/src/models/Message.model.ts +++ b/src/models/Message.model.ts @@ -8,6 +8,8 @@ export enum MainMessage { GAME_MODE = 'GAME_MODE', SERVER_FULL = 'SERVER_FULL', WRONG_VERSION = 'WRONG_VERSION', + AUTHENTICATION_ACCEPTED = 'AUTHENTICATION_ACCEPTED', + AUTHENTICATION_DENIED = 'AUTHENTICATION_DENIED', CHAT_GLOBAL = 'CHAT_GLOBAL', CHAT_COMMAND = 'CHAT_COMMAND', SET_CONNECTION_ERROR = 'SET_CONNECTION_ERROR', diff --git a/src/models/State.model.ts b/src/models/State.model.ts index 01f316c..8759778 100644 --- a/src/models/State.model.ts +++ b/src/models/State.model.ts @@ -24,6 +24,8 @@ export type RouterState = Readonly export interface ConnectionStateDraft { server: Server | null + authenticated: boolean + authenticationThrottle: number hasToken: boolean error: string } diff --git a/src/renderer/Connector.ts b/src/renderer/Connector.ts index e933b3e..2759e48 100644 --- a/src/renderer/Connector.ts +++ b/src/renderer/Connector.ts @@ -3,7 +3,17 @@ import { push } from 'react-router-redux' import { store } from '.' import { addGlobalMessage, clearGlobalMessages } from './utils/chat.util' -import { disconnect, setConnectionError, setPlayer, setPlayers, setServer, setGameMode } from './actions/connection' +import { + authenticationAccepted, + authenticationDenied, + authenticationRequired, + disconnect, + setConnectionError, + setGameMode, + setPlayer, + setPlayers, + setServer +} from './actions/connection' import { isConnectedToEmulator, setEmulatorError } from './actions/emulator' import { MainMessage, RendererMessage } from '../models/Message.model' import { Server } from '../models/Server.model' @@ -20,6 +30,8 @@ export class Connector { ipcRenderer.on(MainMessage.GAME_MODE, this.onSetGameMode) ipcRenderer.on(MainMessage.SERVER_FULL, this.onServerFull) ipcRenderer.on(MainMessage.WRONG_VERSION, this.onWrongVersion) + ipcRenderer.on(MainMessage.AUTHENTICATION_ACCEPTED, this.onAuthenticationAccepted) + ipcRenderer.on(MainMessage.AUTHENTICATION_DENIED, this.onAuthenticationDenied) ipcRenderer.on(MainMessage.CHAT_GLOBAL, this.onGlobalChatMessage) ipcRenderer.on(MainMessage.CHAT_COMMAND, this.onCommandMessage) ipcRenderer.on(MainMessage.SET_CONNECTION_ERROR, this.onConnectionError) @@ -49,6 +61,9 @@ export class Connector { console.info('CONNECTED TO SERVER', server) } store.dispatch(setServer(server)) + if (server.passwordRequired) { + store.dispatch(authenticationRequired()) + } } private onSetPlayers = (_: Electron.Event, players: IPlayerUpdate[]) => { @@ -72,7 +87,7 @@ export class Connector { } private onWrongVersion = ( - event: Electron.Event, + _: Electron.Event, { majorVersion, minorVersion }: { majorVersion: number, minorVersion: number} ) => { @@ -80,6 +95,14 @@ export class Connector { // TODO add server version -> client version mapping } + private onAuthenticationAccepted = () => { + store.dispatch(authenticationAccepted()) + } + + private onAuthenticationDenied = (_: Electron.Event, throttle: number) => { + store.dispatch(authenticationDenied(throttle)) + } + private onGlobalChatMessage = ( _: Electron.Event, { message, senderId }: diff --git a/src/renderer/actions/connection.ts b/src/renderer/actions/connection.ts index 461f50c..89fa98c 100644 --- a/src/renderer/actions/connection.ts +++ b/src/renderer/actions/connection.ts @@ -4,6 +4,9 @@ import { SetPlayersAction, SetPlayerAction, SetGameModeAction, + AuthenticationRequired, + AuthenticationAccepted, + AuthenticationDenied, DisconnectAction, ConnectionActionType } from './models/connection.model' @@ -46,6 +49,25 @@ export function setGameMode (gameMode: number): SetGameModeAction { } } +export function authenticationRequired (): AuthenticationRequired { + return { + type: ConnectionActionType.AUTHENTICATION_REQUIRED + } +} + +export function authenticationAccepted (): AuthenticationAccepted { + return { + type: ConnectionActionType.AUTHENTICATION_ACCEPTED + } +} + +export function authenticationDenied (throttle: number): AuthenticationDenied { + return { + type: ConnectionActionType.AUTHENTICATION_DENIED, + throttle + } +} + export function disconnect (): DisconnectAction { return { type: ConnectionActionType.DISCONNECT diff --git a/src/renderer/actions/models/connection.model.ts b/src/renderer/actions/models/connection.model.ts index 2ebe0ea..3648449 100644 --- a/src/renderer/actions/models/connection.model.ts +++ b/src/renderer/actions/models/connection.model.ts @@ -24,6 +24,14 @@ export interface SetGameModeAction extends Action { gameMode: number } +export type AuthenticationRequired = Action + +export type AuthenticationAccepted = Action + +export interface AuthenticationDenied extends Action { + throttle: number +} + export type DisconnectAction = Action export type ConnectionAction = @@ -32,6 +40,9 @@ export type ConnectionAction = & SetPlayersAction & SetPlayerAction & SetGameModeAction + & AuthenticationRequired + & AuthenticationAccepted + & AuthenticationDenied & DisconnectAction export enum ConnectionActionType { @@ -40,5 +51,8 @@ export enum ConnectionActionType { SET_PLAYERS = 'SET_PLAYERS', SET_PLAYER = 'SET_PLAYER', GAME_MODE = 'GAME_MODE', + AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED', + AUTHENTICATION_ACCEPTED = 'AUTHENTICATION_ACCEPTED', + AUTHENTICATION_DENIED = 'AUTHENTICATION_DENIED', DISCONNECT = 'DISCONNECT' } diff --git a/src/renderer/components/areas/ConnectionArea.tsx b/src/renderer/components/areas/ConnectionArea.tsx index 518fb92..ff1bcdb 100644 --- a/src/renderer/components/areas/ConnectionArea.tsx +++ b/src/renderer/components/areas/ConnectionArea.tsx @@ -1,29 +1,23 @@ import * as React from 'react' +import { Dispatch } from 'redux' +import { connect } from 'react-redux' import { SendPasswordArea } from './SendPasswordArea' import { ServerPanel } from '../panels/ServerPanel' import { ChatArea } from '../areas/ChatArea' +import { State } from '../../../models/State.model' import { Server } from '../../../models/Server.model' interface ConnectionAreaProps { + dispatch: Dispatch server: Server + authenticated: boolean + authenticationThrottle: number } -interface ConnectionAreaState { - passwordAccepted: boolean -} - -export class ConnectionArea extends React.PureComponent { - constructor (props: ConnectionAreaProps) { - super(props) - this.state = { - passwordAccepted: !props.server.passwordRequired - } - } - +class Area extends React.PureComponent { public render (): JSX.Element { - const { server } = this.props - const { passwordAccepted } = this.state + const { server, authenticated, authenticationThrottle } = this.props const styles: React.CSSProperties = { area: { overflowY: 'auto', @@ -39,10 +33,16 @@ export class ConnectionArea extends React.PureComponent { - !passwordAccepted && - + !authenticated && + } ) } } +export const ConnectionArea = connect((state: State) => ({ + authenticated: state.connection.authenticated, + authenticationThrottle: state.connection.authenticationThrottle +}))(Area) diff --git a/src/renderer/components/areas/SendPasswordArea.tsx b/src/renderer/components/areas/SendPasswordArea.tsx index e643187..8f46fa4 100644 --- a/src/renderer/components/areas/SendPasswordArea.tsx +++ b/src/renderer/components/areas/SendPasswordArea.tsx @@ -14,17 +14,22 @@ export const MAX_LENGTH_PASSWORD = 30 interface SendPasswordProps { dispatch: Dispatch + throttle: number } interface SendPasswordAreaState { password: string + timeout: string } class Area extends React.PureComponent { + private throttleUpdateInterval?: NodeJS.Timer + constructor (props: SendPasswordProps) { super(props) this.state = { - password: '' + password: '', + timeout: '' } this.onPasswordChange = this.onPasswordChange.bind(this) this.onKeyPress = this.onKeyPress.bind(this) @@ -32,6 +37,39 @@ class Area extends React.PureComponent this.onDisconnect = this.onDisconnect.bind(this) } + public componentDidMount (): void { + if (!this.props.throttle || this.throttleUpdateInterval) return + this.startThrottleUpdate() + } + + public componentWillReceiveProps (nextProps: SendPasswordProps): void { + if (nextProps.throttle == null || this.throttleUpdateInterval) return + this.startThrottleUpdate() + } + + private startThrottleUpdate (): void { + this.throttleUpdateInterval = setInterval(() => { + const { throttle } = this.props + const remainingThrottleTime = (throttle - Date.now()) / 1000 + if (remainingThrottleTime <= 0) { + this.setState({ + timeout: '' + }) + return + } + const timeout = String(Math.trunc(Math.ceil(remainingThrottleTime))) + this.setState({ + timeout + }) + }, 100) + } + + public componentWillUnmount (): void { + if (!this.throttleUpdateInterval) return + clearInterval(this.throttleUpdateInterval) + delete this.throttleUpdateInterval + } + private onPasswordChange ({ target }: React.ChangeEvent): void { let value = target.value if (value.length > MAX_LENGTH_PASSWORD) { @@ -59,7 +97,7 @@ class Area extends React.PureComponent } public render (): JSX.Element { - const { password } = this.state + const { password, timeout } = this.state return (
@@ -74,8 +112,17 @@ class Area extends React.PureComponent onChange={this.onPasswordChange} onKeyPress={this.onKeyPress} /> + { + timeout && + + }
- + void onMouseLeave?: () => void @@ -64,10 +68,8 @@ export class SMMButton extends React.PureComponent diff --git a/src/renderer/reducers/connection.ts b/src/renderer/reducers/connection.ts index 3f149b5..0444633 100644 --- a/src/renderer/reducers/connection.ts +++ b/src/renderer/reducers/connection.ts @@ -33,6 +33,15 @@ export const connection = (state: ConnectionState = initialState.connection, act if (!draft.server) return draft.server.gameMode = action.gameMode break + case ConnectionActionType.AUTHENTICATION_REQUIRED: + draft.authenticated = false + break + case ConnectionActionType.AUTHENTICATION_ACCEPTED: + draft.authenticated = true + break + case ConnectionActionType.AUTHENTICATION_DENIED: + draft.authenticationThrottle = Date.now() + action.throttle * 1000 + break case ConnectionActionType.DISCONNECT: draft.server = null break diff --git a/src/renderer/reducers/index.ts b/src/renderer/reducers/index.ts index e7bdbe9..f1576aa 100644 --- a/src/renderer/reducers/index.ts +++ b/src/renderer/reducers/index.ts @@ -43,6 +43,8 @@ export function initReducer (history: History, electronSave: SaveState): Store