diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-11-15 11:49:22 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-03-14 13:58:44 +0100 |
| commit | 9916f3444c1d15a6d9ad5caba3555e1c0e1a704c (patch) | |
| tree | 74ae736686f5e86409fe9a46a02030c569d9ada2 /gui/src | |
| parent | dd8f146536a4bea945006cfe61f92e3ee4b8e584 (diff) | |
| download | mullvadvpn-9916f3444c1d15a6d9ad5caba3555e1c0e1a704c.tar.xz mullvadvpn-9916f3444c1d15a6d9ad5caba3555e1c0e1a704c.zip | |
Show revoked view when account has been revoked
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 10 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 15 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 65 | ||||
| -rw-r--r-- | gui/src/renderer/components/MainView.tsx | 33 | ||||
| -rw-r--r-- | gui/src/renderer/redux/account/actions.ts | 12 | ||||
| -rw-r--r-- | gui/src/renderer/redux/account/reducers.ts | 13 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 7 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 3 |
8 files changed, 100 insertions, 58 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 8eace70f8b..bb90d77d52 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -49,6 +49,7 @@ import { DeviceConfig, IDevice, IDeviceRemoval, + IDeviceEvent, } from '../shared/daemon-rpc-types'; import log from '../shared/logging'; @@ -1152,7 +1153,7 @@ function convertFromDaemonEvent(data: grpcTypes.DaemonEvent): DaemonEvent { const deviceConfig = data.getDevice(); if (deviceConfig !== undefined) { - return { deviceConfig: convertFromDeviceEvent(deviceConfig) }; + return { device: convertFromDeviceEvent(deviceConfig) }; } const deviceRemoval = data.getRemoveDevice(); @@ -1367,8 +1368,11 @@ function convertToTransportProtocol(protocol: RelayProtocol): grpcTypes.Transpor } } -function convertFromDeviceEvent(deviceEvent: grpcTypes.DeviceEvent): DeviceConfig { - return convertFromDeviceConfig(deviceEvent.getDevice()); +function convertFromDeviceEvent(deviceEvent: grpcTypes.DeviceEvent): IDeviceEvent { + return { + deviceConfig: convertFromDeviceConfig(deviceEvent.getDevice()), + remote: deviceEvent.getRemote(), + }; } function convertFromDeviceConfig(deviceConfig?: grpcTypes.DeviceConfig): DeviceConfig { diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index d2264242cc..9bb988a199 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -36,6 +36,7 @@ import { RelaySettings, RelaySettingsUpdate, TunnelState, + IDeviceEvent, } from '../shared/daemon-rpc-types'; import { messages, relayLocations } from '../shared/gettext'; import { SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state'; @@ -643,7 +644,7 @@ class ApplicationMain { // fetch device try { - this.setDeviceConfig(await this.daemonRpc.getDevice()); + this.setDeviceConfig({ deviceConfig: await this.daemonRpc.getDevice() }); } catch (e) { const error = e as Error; log.error(`Failed to fetch device: ${error.message}`); @@ -779,8 +780,8 @@ class ApplicationMain { ); } else if ('appVersionInfo' in daemonEvent) { this.setLatestVersion(daemonEvent.appVersionInfo); - } else if ('deviceConfig' in daemonEvent) { - this.setDeviceConfig(daemonEvent.deviceConfig); + } else if ('device' in daemonEvent) { + this.setDeviceConfig(daemonEvent.device); } else if ('deviceRemoval' in daemonEvent) { if (this.windowController) { IpcMainEventChannel.account.notifyDevices( @@ -1112,20 +1113,20 @@ class ApplicationMain { } } - private setDeviceConfig(deviceConfig: DeviceConfig) { + private setDeviceConfig(deviceEvent: IDeviceEvent) { const oldDeviceConfig = this.deviceConfig; - this.deviceConfig = deviceConfig; + this.deviceConfig = deviceEvent.deviceConfig; // make sure to invalidate the account data cache when account tokens change this.updateAccountDataOnAccountChange( oldDeviceConfig?.accountToken, - deviceConfig?.accountToken, + deviceEvent.deviceConfig?.accountToken, ); void this.updateAccountHistory(); if (this.windowController) { - IpcMainEventChannel.account.notifyDevice(this.windowController.webContents, deviceConfig); + IpcMainEventChannel.account.notifyDevice(this.windowController.webContents, deviceEvent); } } diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 586e120868..47a58fc4dd 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -42,6 +42,7 @@ import { RelaySettingsUpdate, TunnelState, VoucherResponse, + IDeviceEvent, } from '../shared/daemon-rpc-types'; import { LogLevel } from '../shared/logging-types'; import IpcOutput from './lib/logging'; @@ -124,9 +125,9 @@ export default class AppRenderer { this.setAccountExpiry(newAccountData?.expiry); }); - IpcRendererEventChannel.account.listenDevice((deviceConfig: DeviceConfig) => { + IpcRendererEventChannel.account.listenDevice((deviceEvent) => { const oldDeviceConfig = this.deviceConfig; - this.handleAccountChange(deviceConfig, oldDeviceConfig?.accountToken); + this.handleAccountChange(deviceEvent, oldDeviceConfig?.accountToken); }); IpcRendererEventChannel.account.listenDevices((devices) => { @@ -200,7 +201,7 @@ export default class AppRenderer { this.setAccountExpiry(initialState.accountData?.expiry); this.setSettings(initialState.settings); - this.handleAccountChange(initialState.deviceConfig, undefined); + this.handleAccountChange({ deviceConfig: initialState.deviceConfig }, undefined); this.setAccountHistory(initialState.accountHistory); this.setTunnelState(initialState.tunnelState); this.updateBlockedState(initialState.tunnelState, initialState.settings.blockWhenDisconnected); @@ -235,10 +236,7 @@ export default class AppRenderer { void this.updateLocation(); - const navigationBase = this.getNavigationBase( - initialState.isConnected, - initialState.deviceConfig?.accountToken, - ); + const navigationBase = this.getNavigationBase(); this.history = new History(navigationBase); } @@ -305,6 +303,13 @@ export default class AppRenderer { } } + public leaveRevokedDevice = async () => { + const reduxAccount = this.reduxActions.account; + reduxAccount.loggedOut(); + this.resetNavigation(); + await this.disconnectTunnel(); + }; + public async createNewAccount() { log.info('Creating account'); @@ -645,13 +650,10 @@ export default class AppRenderer { private resetNavigation() { if (this.history) { const pathname = this.history.location.pathname as RoutePath; - const nextPath = this.getNavigationBase( - this.connectedToDaemon, - this.deviceConfig?.accountToken, - ) as RoutePath; + const nextPath = this.getNavigationBase() as RoutePath; - // First level contains the possible next locations and the second level contains the possible - // current locations. + // First level contains the possible next locations and the second level contains the + // possible current locations. const navigationTransitions: Partial< Record<RoutePath, Partial<Record<RoutePath | '*', ITransitionSpecification>>> > = { @@ -679,9 +681,11 @@ export default class AppRenderer { } } - private getNavigationBase(connectedToDaemon: boolean, accountToken?: string): RoutePath { - if (connectedToDaemon) { - return accountToken ? RoutePath.main : RoutePath.login; + private getNavigationBase(): RoutePath { + if (this.connectedToDaemon) { + const loginState = this.reduxStore.getState().account.status; + const deviceRevoked = loginState.type === 'none' && loginState.deviceRevoked; + return this.deviceConfig?.accountToken || deviceRevoked ? RoutePath.main : RoutePath.login; } else { return RoutePath.launch; } @@ -770,36 +774,31 @@ export default class AppRenderer { } } - private handleAccountChange(newDeviceConfig: DeviceConfig, oldAccount?: string) { + private handleAccountChange(newDeviceEvent: IDeviceEvent, oldAccount?: string) { const reduxAccount = this.reduxActions.account; - this.deviceConfig = newDeviceConfig; - const newAccount = newDeviceConfig?.accountToken; + this.deviceConfig = newDeviceEvent.deviceConfig; + const newAccount = newDeviceEvent.deviceConfig?.accountToken; + const newDevice = newDeviceEvent.deviceConfig?.device; if (oldAccount && !newAccount) { this.loginScheduler.cancel(); - reduxAccount.loggedOut(); + if (newDeviceEvent.remote) { + reduxAccount.deviceRevoked(); + } else { + reduxAccount.loggedOut(); + } this.resetNavigation(); - } else if ( - newDeviceConfig?.accountToken !== undefined && - newDeviceConfig?.device !== undefined && - oldAccount !== newAccount - ) { + } else if (newAccount !== undefined && newDevice !== undefined && oldAccount !== newAccount) { switch (this.loginState) { case 'none': case 'logging in': - reduxAccount.loggedIn({ - accountToken: newDeviceConfig.accountToken, - device: newDeviceConfig.device, - }); + reduxAccount.loggedIn({ accountToken: newAccount, device: newDevice }); break; case 'creating account': reduxAccount.accountCreated( - { - accountToken: newDeviceConfig.accountToken, - device: newDeviceConfig.device, - }, + { accountToken: newAccount, device: newDevice }, new Date().toISOString(), ); break; diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx index c7a8851a90..9ced798cf9 100644 --- a/gui/src/renderer/components/MainView.tsx +++ b/gui/src/renderer/components/MainView.tsx @@ -1,29 +1,44 @@ import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; import { hasExpired } from '../../shared/account-expiry'; -import { IReduxState } from '../redux/store'; +import { useSelector } from '../redux/store'; import ConnectPage from '../containers/ConnectPage'; import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer'; import { useHistory } from '../lib/history'; import { RoutePath } from '../lib/routes'; +import { DeviceRevokedView } from './DeviceRevokedView'; export default function MainView() { const history = useHistory(); - const accountExpiry = useSelector((state: IReduxState) => state.account.expiry); - const accountHasExpired = accountExpiry && hasExpired(accountExpiry); + const accountExpiry = useSelector((state) => state.account.expiry); + const accountHasExpired = accountExpiry !== undefined && hasExpired(accountExpiry); const isNewAccount = useSelector( - (state: IReduxState) => - state.account.status.type === 'ok' && state.account.status.method === 'new_account', + (state) => state.account.status.type === 'ok' && state.account.status.method === 'new_account', + ); + const showDeviceRevoked = useSelector( + (state) => + (state.connection.status.state === 'error' && + state.connection.status.details.cause.reason === 'tunnel_parameter_error' && + state.connection.status.details.cause.details === 'no_wireguard_key') || + (state.account.status.type === 'none' && state.account.status.deviceRevoked), + ); + + const [showAccountExpired, setShowAccountExpired] = useState<boolean>( + (isNewAccount || accountHasExpired) && !showDeviceRevoked, ); - const [showAccountExpired, setShowAccountExpired] = useState(isNewAccount || accountHasExpired); useEffect(() => { - if (accountHasExpired) { + if (accountHasExpired && !showDeviceRevoked) { setShowAccountExpired(true); } else if (showAccountExpired && !accountHasExpired) { history.push(RoutePath.timeAdded); } }, [showAccountExpired, accountHasExpired]); - return showAccountExpired ? <ExpiredAccountErrorViewContainer /> : <ConnectPage />; + if (showDeviceRevoked) { + return <DeviceRevokedView />; + } else if (showAccountExpired) { + return <ExpiredAccountErrorViewContainer />; + } else { + return <ConnectPage />; + } } diff --git a/gui/src/renderer/redux/account/actions.ts b/gui/src/renderer/redux/account/actions.ts index e53cb11545..5bc3e84aa9 100644 --- a/gui/src/renderer/redux/account/actions.ts +++ b/gui/src/renderer/redux/account/actions.ts @@ -29,6 +29,10 @@ interface IResetLoginErrorAction { type: 'RESET_LOGIN_ERROR'; } +interface IDeviceRevokedAction { + type: 'DEVICE_REVOKED'; +} + interface IStartCreateAccount { type: 'START_CREATE_ACCOUNT'; } @@ -76,6 +80,7 @@ export type AccountAction = | ILoginTooManyDevicesAction | ILoggedOutAction | IResetLoginErrorAction + | IDeviceRevokedAction | IStartCreateAccount | ICreateAccountFailed | IAccountCreated @@ -126,6 +131,12 @@ function resetLoginError(): IResetLoginErrorAction { }; } +function deviceRevoked(): IDeviceRevokedAction { + return { + type: 'DEVICE_REVOKED', + }; +} + function startCreateAccount(): IStartCreateAccount { return { type: 'START_CREATE_ACCOUNT', @@ -187,6 +198,7 @@ export default { loginTooManyDevices, loggedOut, resetLoginError, + deviceRevoked, startCreateAccount, createAccountFailed, accountCreated, diff --git a/gui/src/renderer/redux/account/reducers.ts b/gui/src/renderer/redux/account/reducers.ts index b336ea665a..5be640e98b 100644 --- a/gui/src/renderer/redux/account/reducers.ts +++ b/gui/src/renderer/redux/account/reducers.ts @@ -3,7 +3,7 @@ import { ReduxAction } from '../store'; type LoginMethod = 'existing_account' | 'new_account'; export type LoginState = - | { type: 'none' } + | { type: 'none'; deviceRevoked: boolean } | { type: 'logging in' | 'ok'; method: LoginMethod } | { type: 'failed' | 'too many devices'; method: LoginMethod; error: Error }; export interface IAccountReduxState { @@ -21,7 +21,7 @@ const initialState: IAccountReduxState = { devices: [], accountHistory: undefined, expiry: undefined, - status: { type: 'none' }, + status: { type: 'none', deviceRevoked: false }, }; export default function ( @@ -56,14 +56,19 @@ export default function ( case 'LOGGED_OUT': return { ...state, - status: { type: 'none' }, + status: { type: 'none', deviceRevoked: false }, accountToken: undefined, expiry: undefined, }; case 'RESET_LOGIN_ERROR': return { ...state, - status: { type: 'none' }, + status: { type: 'none', deviceRevoked: false }, + }; + case 'DEVICE_REVOKED': + return { + ...state, + status: { type: 'none', deviceRevoked: true }, }; case 'START_CREATE_ACCOUNT': return { diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 6fe598c7a0..7840a68d26 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -105,7 +105,7 @@ export type DaemonEvent = | { settings: ISettings } | { relayList: IRelayList } | { appVersionInfo: IAppVersionInfo } - | { deviceConfig: DeviceConfig } + | { device: IDeviceEvent } | { deviceRemoval: Array<IDevice> }; export interface ITunnelStateRelayInfo { @@ -322,6 +322,11 @@ export interface IAppVersionInfo { suggestedIsBeta?: boolean; } +export interface IDeviceEvent { + deviceConfig: DeviceConfig; + remote?: boolean; +} + export type DeviceConfig = | undefined | { diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index 93947bee62..62dd206c4c 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -16,6 +16,7 @@ import { RelaySettingsUpdate, TunnelState, VoucherResponse, + IDeviceEvent, } from './daemon-rpc-types'; import { IGuiSettingsState } from './gui-settings-state'; import { LogLevel } from './logging-types'; @@ -167,7 +168,7 @@ export const ipcSchema = { }, account: { '': notifyRenderer<IAccountData | undefined>(), - device: notifyRenderer<DeviceConfig>(), + device: notifyRenderer<IDeviceEvent>(), devices: notifyRenderer<Array<IDevice>>(), create: invoke<void, string>(), login: invoke<AccountToken, void>(), |
