diff --git a/src/domain/ForcedLogoutViewModel.ts b/src/domain/ForcedLogoutViewModel.ts new file mode 100644 index 00000000..87c71418 --- /dev/null +++ b/src/domain/ForcedLogoutViewModel.ts @@ -0,0 +1,55 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {Options as BaseOptions, ViewModel} from "./ViewModel"; +import {Client} from "../matrix/Client.js"; +import {SegmentType} from "./navigation/index"; + +type Options = { sessionId: string; } & BaseOptions; + +export class ForcedLogoutViewModel extends ViewModel { + private _sessionId: string; + private _error?: Error; + private _logoutPromise: Promise; + + constructor(options: Options) { + super(options); + this._sessionId = options.sessionId; + this._logoutPromise = this.forceLogout(); + } + + async forceLogout(): Promise { + try { + const client = new Client(this.platform); + await client.startForcedLogout(this._sessionId); + } + catch (err) { + this._error = err; + this.emitChange("error"); + } + } + + async proceed(): Promise { + await this._logoutPromise; + this.navigation.push("session", true); + } + + get error(): string | undefined { + if (this._error) { + return this.i18n`Could not log out of device: ${this._error.message}`; + } + } +} diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 4094d864..c13f7ce6 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -19,6 +19,7 @@ import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {LoginViewModel} from "./login/LoginViewModel"; import {LogoutViewModel} from "./LogoutViewModel"; +import {ForcedLogoutViewModel} from "./ForcedLogoutViewModel"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {ViewModel} from "./ViewModel"; @@ -30,6 +31,7 @@ export class RootViewModel extends ViewModel { this._sessionLoadViewModel = null; this._loginViewModel = null; this._logoutViewModel = null; + this._forcedLogoutViewModel = null; this._sessionViewModel = null; this._pendingClient = null; } @@ -38,12 +40,14 @@ export class RootViewModel extends ViewModel { this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("sso").subscribe(() => this._applyNavigation())); + this.track(this.navigation.observe("forced-logout").subscribe(() => this._applyNavigation())); this._applyNavigation(true); } async _applyNavigation(shouldRestoreLastUrl) { const isLogin = this.navigation.path.get("login"); const logoutSessionId = this.navigation.path.get("logout")?.value; + const forcedLogoutSessionId = this.navigation.path.get("forced-logout")?.value; const sessionId = this.navigation.path.get("session")?.value; const loginToken = this.navigation.path.get("sso")?.value; if (isLogin) { @@ -54,6 +58,10 @@ export class RootViewModel extends ViewModel { if (this.activeSection !== "logout") { this._showLogout(logoutSessionId); } + } else if (forcedLogoutSessionId) { + if (this.activeSection !== "forced-logout") { + this._showForcedLogout(forcedLogoutSessionId); + } } else if (sessionId === true) { if (this.activeSection !== "picker") { this._showPicker(); @@ -136,6 +144,12 @@ export class RootViewModel extends ViewModel { }); } + _showForcedLogout(sessionId) { + this._setSection(() => { + this._forcedLogoutViewModel = new ForcedLogoutViewModel(this.childOptions({sessionId})); + }); + } + _showSession(client) { this._setSection(() => { this._sessionViewModel = new SessionViewModel(this.childOptions({client})); @@ -164,6 +178,8 @@ export class RootViewModel extends ViewModel { return "login"; } else if (this._logoutViewModel) { return "logout"; + } else if (this._forcedLogoutViewModel) { + return "forced-logout"; } else if (this._sessionPickerViewModel) { return "picker"; } else if (this._sessionLoadViewModel) { @@ -180,6 +196,7 @@ export class RootViewModel extends ViewModel { this._sessionLoadViewModel = this.disposeTracked(this._sessionLoadViewModel); this._loginViewModel = this.disposeTracked(this._loginViewModel); this._logoutViewModel = this.disposeTracked(this._logoutViewModel); + this._forcedLogoutViewModel = this.disposeTracked(this._forcedLogoutViewModel); this._sessionViewModel = this.disposeTracked(this._sessionViewModel); // now set it again setter(); @@ -187,6 +204,7 @@ export class RootViewModel extends ViewModel { this._sessionLoadViewModel && this.track(this._sessionLoadViewModel); this._loginViewModel && this.track(this._loginViewModel); this._logoutViewModel && this.track(this._logoutViewModel); + this._forcedLogoutViewModel && this.track(this._forcedLogoutViewModel); this._sessionViewModel && this.track(this._sessionViewModel); this.emitChange("activeSection"); } @@ -195,6 +213,7 @@ export class RootViewModel extends ViewModel { get sessionViewModel() { return this._sessionViewModel; } get loginViewModel() { return this._loginViewModel; } get logoutViewModel() { return this._logoutViewModel; } + get forcedLogoutViewModel() { return this._forcedLogoutViewModel; } get sessionPickerViewModel() { return this._sessionPickerViewModel; } get sessionLoadViewModel() { return this._sessionLoadViewModel; } } diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index afba0d86..c1fbee13 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -23,6 +23,7 @@ export type SegmentType = { "session": string | boolean; "sso": string; "logout": true; + "forced-logout": true; "room": string; "rooms": string[]; "settings": true; @@ -48,7 +49,9 @@ function allowsChild(parent: Segment | undefined, child: Segment { + this._sessionId = sessionId; + log.set("id", this._sessionId); + await this.deleteSession(log); + }); + } + dispose() { if (this._reconnectSubscription) { this._reconnectSubscription(); diff --git a/src/platform/web/ui/ForcedLogoutView.js b/src/platform/web/ui/ForcedLogoutView.js new file mode 100644 index 00000000..fe18dac3 --- /dev/null +++ b/src/platform/web/ui/ForcedLogoutView.js @@ -0,0 +1,45 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView, InlineTemplateView} from "./general/TemplateView"; + +export class ForcedLogoutView extends TemplateView { + render(t, vm) { + const proceedView = new InlineTemplateView(vm, t => { + return t.div([ + t.p("Your access token is no longer valid! You can reauthenticate in the next screen."), + t.div({ className: "button-row" }, [ + t.button({ + className: "button-action primary", + type: "submit", + onClick: () => vm.proceed(), + }, vm.i18n`Proceed`) + ]), + ]); + }); + const progressView = new InlineTemplateView(vm, t => { + return t.p({className: "status"}, [ t.span(vm => vm.error) ]); + }); + + return t.div({className: "LogoutScreen"}, [ + t.div({className: "content"}, + t.mapView(vm => vm.error, error => { + return error? progressView: proceedView; + }) + ), + ]); + } +} diff --git a/src/platform/web/ui/RootView.js b/src/platform/web/ui/RootView.js index 69b327a5..1db5c334 100644 --- a/src/platform/web/ui/RootView.js +++ b/src/platform/web/ui/RootView.js @@ -17,6 +17,7 @@ limitations under the License. import {SessionView} from "./session/SessionView.js"; import {LoginView} from "./login/LoginView"; import {LogoutView} from "./LogoutView.js"; +import {ForcedLogoutView} from "./ForcedLogoutView.js"; import {SessionLoadView} from "./login/SessionLoadView.js"; import {SessionPickerView} from "./login/SessionPickerView.js"; import {TemplateView} from "./general/TemplateView"; @@ -39,6 +40,8 @@ export class RootView extends TemplateView { return new LoginView(vm.loginViewModel); case "logout": return new LogoutView(vm.logoutViewModel); + case "forced-logout": + return new ForcedLogoutView(vm.forcedLogoutViewModel); case "picker": return new SessionPickerView(vm.sessionPickerViewModel); case "redirecting":