Merge pull request #453 from MidhunSureshR/sso-login
[SSO] - [PR 4] - SSO/Token login functionality
This commit is contained in:
commit
3b693c5b02
23 changed files with 808 additions and 182 deletions
|
@ -1,84 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
|
||||||
|
|
||||||
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 {ViewModel} from "./ViewModel.js";
|
|
||||||
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
|
|
||||||
|
|
||||||
export class LoginViewModel extends ViewModel {
|
|
||||||
constructor(options) {
|
|
||||||
super(options);
|
|
||||||
const {ready, defaultHomeServer, createSessionContainer} = options;
|
|
||||||
this._createSessionContainer = createSessionContainer;
|
|
||||||
this._ready = ready;
|
|
||||||
this._defaultHomeServer = defaultHomeServer;
|
|
||||||
this._sessionContainer = null;
|
|
||||||
this._loadViewModel = null;
|
|
||||||
this._loadViewModelSubscription = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get defaultHomeServer() { return this._defaultHomeServer; }
|
|
||||||
|
|
||||||
get loadViewModel() {return this._loadViewModel; }
|
|
||||||
|
|
||||||
get isBusy() {
|
|
||||||
if (!this._loadViewModel) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return this._loadViewModel.loading;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(username, password, homeserver) {
|
|
||||||
this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription);
|
|
||||||
if (this._loadViewModel) {
|
|
||||||
this._loadViewModel = this.disposeTracked(this._loadViewModel);
|
|
||||||
}
|
|
||||||
this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({
|
|
||||||
createAndStartSessionContainer: () => {
|
|
||||||
this._sessionContainer = this._createSessionContainer();
|
|
||||||
this._sessionContainer.startWithLogin(homeserver, username, password);
|
|
||||||
return this._sessionContainer;
|
|
||||||
},
|
|
||||||
ready: sessionContainer => {
|
|
||||||
// make sure we don't delete the session in dispose when navigating away
|
|
||||||
this._sessionContainer = null;
|
|
||||||
this._ready(sessionContainer);
|
|
||||||
},
|
|
||||||
homeserver,
|
|
||||||
})));
|
|
||||||
this._loadViewModel.start();
|
|
||||||
this.emitChange("loadViewModel");
|
|
||||||
this._loadViewModelSubscription = this.track(this._loadViewModel.disposableOn("change", () => {
|
|
||||||
if (!this._loadViewModel.loading) {
|
|
||||||
this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription);
|
|
||||||
}
|
|
||||||
this.emitChange("isBusy");
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
get cancelUrl() {
|
|
||||||
return this.urlCreator.urlForSegment("session");
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
super.dispose();
|
|
||||||
if (this._sessionContainer) {
|
|
||||||
// if we move away before we're done with initial sync
|
|
||||||
// delete the session
|
|
||||||
this._sessionContainer.deleteSession();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import {SessionViewModel} from "./session/SessionViewModel.js";
|
import {SessionViewModel} from "./session/SessionViewModel.js";
|
||||||
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
|
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
|
||||||
import {LoginViewModel} from "./LoginViewModel.js";
|
import {LoginViewModel} from "./login/LoginViewModel.js";
|
||||||
import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
|
import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
|
||||||
import {ViewModel} from "./ViewModel.js";
|
import {ViewModel} from "./ViewModel.js";
|
||||||
|
|
||||||
|
@ -35,12 +35,14 @@ export class RootViewModel extends ViewModel {
|
||||||
async load() {
|
async load() {
|
||||||
this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation()));
|
this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation()));
|
||||||
this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation()));
|
this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation()));
|
||||||
|
this.track(this.navigation.observe("sso").subscribe(() => this._applyNavigation()));
|
||||||
this._applyNavigation(true);
|
this._applyNavigation(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _applyNavigation(shouldRestoreLastUrl) {
|
async _applyNavigation(shouldRestoreLastUrl) {
|
||||||
const isLogin = this.navigation.observe("login").get();
|
const isLogin = this.navigation.path.get("login")
|
||||||
const sessionId = this.navigation.observe("session").get();
|
const sessionId = this.navigation.path.get("session")?.value;
|
||||||
|
const loginToken = this.navigation.path.get("sso")?.value;
|
||||||
if (isLogin) {
|
if (isLogin) {
|
||||||
if (this.activeSection !== "login") {
|
if (this.activeSection !== "login") {
|
||||||
this._showLogin();
|
this._showLogin();
|
||||||
|
@ -65,7 +67,13 @@ export class RootViewModel extends ViewModel {
|
||||||
this._showSessionLoader(sessionId);
|
this._showSessionLoader(sessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (loginToken) {
|
||||||
|
this.urlCreator.normalizeUrl();
|
||||||
|
if (this.activeSection !== "login") {
|
||||||
|
this._showLogin(loginToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
try {
|
try {
|
||||||
if (!(shouldRestoreLastUrl && this.urlCreator.tryRestoreLastUrl())) {
|
if (!(shouldRestoreLastUrl && this.urlCreator.tryRestoreLastUrl())) {
|
||||||
const sessionInfos = await this.platform.sessionInfoStorage.getAll();
|
const sessionInfos = await this.platform.sessionInfoStorage.getAll();
|
||||||
|
@ -94,7 +102,7 @@ export class RootViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_showLogin() {
|
_showLogin(loginToken) {
|
||||||
this._setSection(() => {
|
this._setSection(() => {
|
||||||
this._loginViewModel = new LoginViewModel(this.childOptions({
|
this._loginViewModel = new LoginViewModel(this.childOptions({
|
||||||
defaultHomeServer: this.platform.config["defaultHomeServer"],
|
defaultHomeServer: this.platform.config["defaultHomeServer"],
|
||||||
|
@ -111,6 +119,7 @@ export class RootViewModel extends ViewModel {
|
||||||
this._pendingSessionContainer = sessionContainer;
|
this._pendingSessionContainer = sessionContainer;
|
||||||
this.navigation.push("session", sessionContainer.sessionId);
|
this.navigation.push("session", sessionContainer.sessionId);
|
||||||
},
|
},
|
||||||
|
loginToken
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -123,13 +132,11 @@ export class RootViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
_showSessionLoader(sessionId) {
|
_showSessionLoader(sessionId) {
|
||||||
|
const sessionContainer = this._createSessionContainer();
|
||||||
|
sessionContainer.startWithExistingSession(sessionId);
|
||||||
this._setSection(() => {
|
this._setSection(() => {
|
||||||
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
||||||
createAndStartSessionContainer: () => {
|
sessionContainer,
|
||||||
const sessionContainer = this._createSessionContainer();
|
|
||||||
sessionContainer.startWithExistingSession(sessionId);
|
|
||||||
return sessionContainer;
|
|
||||||
},
|
|
||||||
ready: sessionContainer => this._showSession(sessionContainer)
|
ready: sessionContainer => this._showSession(sessionContainer)
|
||||||
}));
|
}));
|
||||||
this._sessionLoadViewModel.start();
|
this._sessionLoadViewModel.start();
|
||||||
|
|
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js";
|
import {LoadStatus} from "../matrix/SessionContainer.js";
|
||||||
import {SyncStatus} from "../matrix/Sync.js";
|
import {SyncStatus} from "../matrix/Sync.js";
|
||||||
import {ViewModel} from "./ViewModel.js";
|
import {ViewModel} from "./ViewModel.js";
|
||||||
|
|
||||||
export class SessionLoadViewModel extends ViewModel {
|
export class SessionLoadViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const {createAndStartSessionContainer, ready, homeserver, deleteSessionOnCancel} = options;
|
const {sessionContainer, ready, homeserver, deleteSessionOnCancel} = options;
|
||||||
this._createAndStartSessionContainer = createAndStartSessionContainer;
|
this._sessionContainer = sessionContainer;
|
||||||
this._ready = ready;
|
this._ready = ready;
|
||||||
this._homeserver = homeserver;
|
this._homeserver = homeserver;
|
||||||
this._deleteSessionOnCancel = deleteSessionOnCancel;
|
this._deleteSessionOnCancel = deleteSessionOnCancel;
|
||||||
|
@ -38,7 +38,6 @@ export class SessionLoadViewModel extends ViewModel {
|
||||||
try {
|
try {
|
||||||
this._loading = true;
|
this._loading = true;
|
||||||
this.emitChange("loading");
|
this.emitChange("loading");
|
||||||
this._sessionContainer = this._createAndStartSessionContainer();
|
|
||||||
this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => {
|
this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => {
|
||||||
this.emitChange("loadLabel");
|
this.emitChange("loadLabel");
|
||||||
// wait for initial sync, but not catchup sync
|
// wait for initial sync, but not catchup sync
|
||||||
|
@ -109,22 +108,9 @@ export class SessionLoadViewModel extends ViewModel {
|
||||||
return `Something went wrong: ${error && error.message}.`;
|
return `Something went wrong: ${error && error.message}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Statuses related to login are handled by respective login view models
|
||||||
if (sc) {
|
if (sc) {
|
||||||
switch (sc.loadStatus.get()) {
|
switch (sc.loadStatus.get()) {
|
||||||
case LoadStatus.NotLoading:
|
|
||||||
return `Preparing…`;
|
|
||||||
case LoadStatus.Login:
|
|
||||||
return `Checking your login and password…`;
|
|
||||||
case LoadStatus.LoginFailed:
|
|
||||||
switch (sc.loginFailure) {
|
|
||||||
case LoginFailure.LoginFailure:
|
|
||||||
return `Your username and/or password don't seem to be correct.`;
|
|
||||||
case LoginFailure.Connection:
|
|
||||||
return `Can't connect to ${this._homeserver}.`;
|
|
||||||
case LoginFailure.Unknown:
|
|
||||||
return `Something went wrong while checking your login and password.`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case LoadStatus.SessionSetup:
|
case LoadStatus.SessionSetup:
|
||||||
return `Setting up your encryption keys…`;
|
return `Setting up your encryption keys…`;
|
||||||
case LoadStatus.Loading:
|
case LoadStatus.Loading:
|
||||||
|
|
76
src/domain/login/CompleteSSOLoginViewModel.js
Normal file
76
src/domain/login/CompleteSSOLoginViewModel.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
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 {ViewModel} from "../ViewModel.js";
|
||||||
|
import {LoginFailure} from "../../matrix/SessionContainer.js";
|
||||||
|
|
||||||
|
export class CompleteSSOLoginViewModel extends ViewModel {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
const {
|
||||||
|
loginToken,
|
||||||
|
sessionContainer,
|
||||||
|
attemptLogin,
|
||||||
|
} = options;
|
||||||
|
this._loginToken = loginToken;
|
||||||
|
this._sessionContainer = sessionContainer;
|
||||||
|
this._attemptLogin = attemptLogin;
|
||||||
|
this._errorMessage = "";
|
||||||
|
this.performSSOLoginCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
get errorMessage() { return this._errorMessage; }
|
||||||
|
|
||||||
|
_showError(message) {
|
||||||
|
this._errorMessage = message;
|
||||||
|
this.emitChange("errorMessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
async performSSOLoginCompletion() {
|
||||||
|
if (!this._loginToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver");
|
||||||
|
let loginOptions;
|
||||||
|
try {
|
||||||
|
loginOptions = await this._sessionContainer.queryLogin(homeserver);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this._showError(err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!loginOptions.token) {
|
||||||
|
this.navigation.push("session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const status = await this._attemptLogin(loginOptions.token(this._loginToken));
|
||||||
|
let error = "";
|
||||||
|
switch (status) {
|
||||||
|
case LoginFailure.Credentials:
|
||||||
|
error = this.i18n`Your login token is invalid.`;
|
||||||
|
break;
|
||||||
|
case LoginFailure.Connection:
|
||||||
|
error = this.i18n`Can't connect to ${homeserver}.`;
|
||||||
|
break;
|
||||||
|
case LoginFailure.Unknown:
|
||||||
|
error = this.i18n`Something went wrong while checking your login token.`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
this._showError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
194
src/domain/login/LoginViewModel.js
Normal file
194
src/domain/login/LoginViewModel.js
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
|
||||||
|
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 {ViewModel} from "../ViewModel.js";
|
||||||
|
import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js";
|
||||||
|
import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js";
|
||||||
|
import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js";
|
||||||
|
import {LoadStatus} from "../../matrix/SessionContainer.js";
|
||||||
|
import {SessionLoadViewModel} from "../SessionLoadViewModel.js";
|
||||||
|
|
||||||
|
export class LoginViewModel extends ViewModel {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
const {ready, defaultHomeServer, createSessionContainer, loginToken} = options;
|
||||||
|
this._createSessionContainer = createSessionContainer;
|
||||||
|
this._ready = ready;
|
||||||
|
this._loginToken = loginToken;
|
||||||
|
this._sessionContainer = this._createSessionContainer();
|
||||||
|
this._loginOptions = null;
|
||||||
|
this._passwordLoginViewModel = null;
|
||||||
|
this._startSSOLoginViewModel = null;
|
||||||
|
this._completeSSOLoginViewModel = null;
|
||||||
|
this._loadViewModel = null;
|
||||||
|
this._loadViewModelSubscription = null;
|
||||||
|
this._homeserver = defaultHomeServer;
|
||||||
|
this._errorMessage = "";
|
||||||
|
this._hideHomeserver = false;
|
||||||
|
this._isBusy = false;
|
||||||
|
this._isFetchingLoginOptions = false;
|
||||||
|
this._createViewModels(this._homeserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
get passwordLoginViewModel() { return this._passwordLoginViewModel; }
|
||||||
|
get startSSOLoginViewModel() { return this._startSSOLoginViewModel; }
|
||||||
|
get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; }
|
||||||
|
get homeserver() { return this._homeserver; }
|
||||||
|
get errorMessage() { return this._errorMessage; }
|
||||||
|
get showHomeserver() { return !this._hideHomeserver; }
|
||||||
|
get loadViewModel() {return this._loadViewModel; }
|
||||||
|
get isBusy() { return this._isBusy; }
|
||||||
|
get isFetchingLoginOptions() { return this._isFetchingLoginOptions; }
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.navigation.push("session");
|
||||||
|
}
|
||||||
|
|
||||||
|
async _createViewModels(homeserver) {
|
||||||
|
if (this._loginToken) {
|
||||||
|
this._hideHomeserver = true;
|
||||||
|
this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel(
|
||||||
|
this.childOptions(
|
||||||
|
{
|
||||||
|
sessionContainer: this._sessionContainer,
|
||||||
|
attemptLogin: loginMethod => this.attemptLogin(loginMethod),
|
||||||
|
loginToken: this._loginToken
|
||||||
|
})));
|
||||||
|
this.emitChange("completeSSOLoginViewModel");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._errorMessage = "";
|
||||||
|
try {
|
||||||
|
this._isFetchingLoginOptions = true;
|
||||||
|
this.emitChange("isFetchingLoginOptions");
|
||||||
|
this._loginOptions = await this._sessionContainer.queryLogin(homeserver);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this._loginOptions = null;
|
||||||
|
}
|
||||||
|
this._isFetchingLoginOptions = false;
|
||||||
|
this.emitChange("isFetchingLoginOptions");
|
||||||
|
if (this._loginOptions) {
|
||||||
|
if (this._loginOptions.sso) { this._showSSOLogin(); }
|
||||||
|
if (this._loginOptions.password) { this._showPasswordLogin(); }
|
||||||
|
if (!this._loginOptions.sso && !this._loginOptions.password) {
|
||||||
|
this._showError("This homeserver neither supports SSO nor Password based login flows");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._showError("Could not query login methods supported by the homeserver");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_showPasswordLogin() {
|
||||||
|
this._passwordLoginViewModel = this.track(new PasswordLoginViewModel(
|
||||||
|
this.childOptions({
|
||||||
|
loginOptions: this._loginOptions,
|
||||||
|
attemptLogin: loginMethod => this.attemptLogin(loginMethod)
|
||||||
|
})));
|
||||||
|
this.emitChange("passwordLoginViewModel");
|
||||||
|
}
|
||||||
|
|
||||||
|
_showSSOLogin() {
|
||||||
|
this._startSSOLoginViewModel = this.track(
|
||||||
|
new StartSSOLoginViewModel(this.childOptions({loginOptions: this._loginOptions}))
|
||||||
|
);
|
||||||
|
this.emitChange("startSSOLoginViewModel");
|
||||||
|
}
|
||||||
|
|
||||||
|
_showError(message) {
|
||||||
|
this._errorMessage = message;
|
||||||
|
this.emitChange("errorMessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
_setBusy(status) {
|
||||||
|
this._isBusy = status;
|
||||||
|
this._passwordLoginViewModel?.setBusy(status);
|
||||||
|
this._startSSOLoginViewModel?.setBusy(status);
|
||||||
|
this.emitChange("isBusy");
|
||||||
|
}
|
||||||
|
|
||||||
|
async attemptLogin(loginMethod) {
|
||||||
|
this._setBusy(true);
|
||||||
|
this._sessionContainer.startWithLogin(loginMethod);
|
||||||
|
const loadStatus = this._sessionContainer.loadStatus;
|
||||||
|
const handle = loadStatus.waitFor(status => status !== LoadStatus.Login);
|
||||||
|
await handle.promise;
|
||||||
|
this._setBusy(false);
|
||||||
|
const status = loadStatus.get();
|
||||||
|
if (status === LoadStatus.LoginFailed) {
|
||||||
|
return this._sessionContainer.loginFailure;
|
||||||
|
}
|
||||||
|
this._hideHomeserver = true;
|
||||||
|
this.emitChange("hideHomeserver");
|
||||||
|
this._disposeViewModels();
|
||||||
|
this._createLoadViewModel();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createLoadViewModel() {
|
||||||
|
this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription);
|
||||||
|
this._loadViewModel = this.disposeTracked(this._loadViewModel);
|
||||||
|
this._loadViewModel = this.track(
|
||||||
|
new SessionLoadViewModel(
|
||||||
|
this.childOptions({
|
||||||
|
ready: (sessionContainer) => {
|
||||||
|
// make sure we don't delete the session in dispose when navigating away
|
||||||
|
this._sessionContainer = null;
|
||||||
|
this._ready(sessionContainer);
|
||||||
|
},
|
||||||
|
sessionContainer: this._sessionContainer,
|
||||||
|
homeserver: this._homeserver
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this._loadViewModel.start();
|
||||||
|
this.emitChange("loadViewModel");
|
||||||
|
this._loadViewModelSubscription = this.track(
|
||||||
|
this._loadViewModel.disposableOn("change", () => {
|
||||||
|
if (!this._loadViewModel.loading) {
|
||||||
|
this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription);
|
||||||
|
}
|
||||||
|
this._setBusy(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposeViewModels() {
|
||||||
|
this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel);
|
||||||
|
this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel);
|
||||||
|
this._completeSSOLoginViewModel = this.disposeTracked(this._completeSSOLoginViewModel);
|
||||||
|
this.emitChange("disposeViewModels");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHomeServer(newHomeserver) {
|
||||||
|
this._errorMessage = "";
|
||||||
|
this.emitChange("errorMessage");
|
||||||
|
this._homeserver = newHomeserver;
|
||||||
|
this._disposeViewModels();
|
||||||
|
this._createViewModels(newHomeserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
super.dispose();
|
||||||
|
if (this._sessionContainer) {
|
||||||
|
// if we move away before we're done with initial sync
|
||||||
|
// delete the session
|
||||||
|
this._sessionContainer.deleteSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
src/domain/login/PasswordLoginViewModel.js
Normal file
64
src/domain/login/PasswordLoginViewModel.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
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 {ViewModel} from "../ViewModel.js";
|
||||||
|
import {LoginFailure} from "../../matrix/SessionContainer.js";
|
||||||
|
|
||||||
|
export class PasswordLoginViewModel extends ViewModel {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
const {loginOptions, attemptLogin} = options;
|
||||||
|
this._loginOptions = loginOptions;
|
||||||
|
this._attemptLogin = attemptLogin;
|
||||||
|
this._isBusy = false;
|
||||||
|
this._errorMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
get isBusy() { return this._isBusy; }
|
||||||
|
get errorMessage() { return this._errorMessage; }
|
||||||
|
|
||||||
|
setBusy(status) {
|
||||||
|
this._isBusy = status;
|
||||||
|
this.emitChange("isBusy");
|
||||||
|
}
|
||||||
|
|
||||||
|
_showError(message) {
|
||||||
|
this._errorMessage = message;
|
||||||
|
this.emitChange("errorMessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(username, password) {
|
||||||
|
this._errorMessage = "";
|
||||||
|
this.emitChange("errorMessage");
|
||||||
|
const loginMethod = this._loginOptions.password(username, password);
|
||||||
|
const status = await this._attemptLogin(loginMethod);
|
||||||
|
let error = "";
|
||||||
|
switch (status) {
|
||||||
|
case LoginFailure.Credentials:
|
||||||
|
error = this.i18n`Your username and/or password don't seem to be correct.`;
|
||||||
|
break;
|
||||||
|
case LoginFailure.Connection:
|
||||||
|
error = this.i18n`Can't connect to ${loginMethod.homeServer}.`;
|
||||||
|
break;
|
||||||
|
case LoginFailure.Unknown:
|
||||||
|
error = this.i18n`Something went wrong while checking your login and password.`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
this._showError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
src/domain/login/StartSSOLoginViewModel.js
Normal file
38
src/domain/login/StartSSOLoginViewModel.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
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 {ViewModel} from "../ViewModel.js";
|
||||||
|
|
||||||
|
export class StartSSOLoginViewModel extends ViewModel{
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this._sso = options.loginOptions.sso;
|
||||||
|
this._isBusy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isBusy() { return this._isBusy; }
|
||||||
|
|
||||||
|
setBusy(status) {
|
||||||
|
this._isBusy = status;
|
||||||
|
this.emitChange("isBusy");
|
||||||
|
}
|
||||||
|
|
||||||
|
async startSSOLogin() {
|
||||||
|
await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._sso.homeserver);
|
||||||
|
const link = this._sso.createSSORedirectURL(this.urlCreator.createSSOCallbackURL());
|
||||||
|
this.platform.openUrl(link);
|
||||||
|
}
|
||||||
|
}
|
|
@ -120,4 +120,14 @@ export class URLRouter {
|
||||||
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
|
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
|
||||||
return this._history.pathAsUrl(urlPath);
|
return this._history.pathAsUrl(urlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createSSOCallbackURL() {
|
||||||
|
return window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeUrl() {
|
||||||
|
// Remove any queryParameters from the URL
|
||||||
|
// Gets rid of the loginToken after SSO
|
||||||
|
this._history.replaceUrlSilently(`${window.location.origin}/${window.location.hash}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ function allowsChild(parent, child) {
|
||||||
switch (parent?.type) {
|
switch (parent?.type) {
|
||||||
case undefined:
|
case undefined:
|
||||||
// allowed root segments
|
// allowed root segments
|
||||||
return type === "login" || type === "session";
|
return type === "login" || type === "session" || type === "sso";
|
||||||
case "session":
|
case "session":
|
||||||
return type === "room" || type === "rooms" || type === "settings";
|
return type === "room" || type === "rooms" || type === "settings";
|
||||||
case "rooms":
|
case "rooms":
|
||||||
|
@ -152,6 +152,10 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
|
||||||
const userId = iterator.next().value;
|
const userId = iterator.next().value;
|
||||||
if (!userId) { break; }
|
if (!userId) { break; }
|
||||||
pushRightPanelSegment(segments, type, userId);
|
pushRightPanelSegment(segments, type, userId);
|
||||||
|
} else if (type.includes("loginToken")) {
|
||||||
|
// Special case for SSO-login with query parameter loginToken=<token>
|
||||||
|
const loginToken = type.split("=").pop();
|
||||||
|
segments.push(new Segment("sso", loginToken));
|
||||||
} else {
|
} else {
|
||||||
// might be undefined, which will be turned into true by Segment
|
// might be undefined, which will be turned into true by Segment
|
||||||
const value = iterator.next().value;
|
const value = iterator.next().value;
|
||||||
|
@ -181,7 +185,8 @@ export function stringifyPath(path) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "right-panel":
|
case "right-panel":
|
||||||
// Ignore right-panel in url
|
case "sso":
|
||||||
|
// Do not put these segments in URL
|
||||||
continue;
|
continue;
|
||||||
default:
|
default:
|
||||||
urlPath += `/${segment.type}`;
|
urlPath += `/${segment.type}`;
|
||||||
|
@ -228,6 +233,12 @@ export function tests() {
|
||||||
const urlPath = stringifyPath(path);
|
const urlPath = stringifyPath(path);
|
||||||
assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details");
|
assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details");
|
||||||
},
|
},
|
||||||
|
"Parse loginToken query parameter into SSO segment": assert => {
|
||||||
|
const segments = parseUrlPath("?loginToken=a1232aSD123");
|
||||||
|
assert.equal(segments.length, 1);
|
||||||
|
assert.equal(segments[0].type, "sso");
|
||||||
|
assert.equal(segments[0].value, "a1232aSD123");
|
||||||
|
},
|
||||||
"parse grid url path with focused empty tile": assert => {
|
"parse grid url path with focused empty tile": assert => {
|
||||||
const segments = parseUrlPath("/session/1/rooms/a,b,c/3");
|
const segments = parseUrlPath("/session/1/rooms/a,b,c/3");
|
||||||
assert.equal(segments.length, 3);
|
assert.equal(segments.length, 3);
|
||||||
|
|
|
@ -23,6 +23,17 @@ import {MediaRepository} from "./net/MediaRepository.js";
|
||||||
import {RequestScheduler} from "./net/RequestScheduler.js";
|
import {RequestScheduler} from "./net/RequestScheduler.js";
|
||||||
import {Sync, SyncStatus} from "./Sync.js";
|
import {Sync, SyncStatus} from "./Sync.js";
|
||||||
import {Session} from "./Session.js";
|
import {Session} from "./Session.js";
|
||||||
|
import {PasswordLoginMethod} from "./login/PasswordLoginMethod.js";
|
||||||
|
import {TokenLoginMethod} from "./login/TokenLoginMethod.js";
|
||||||
|
import {SSOLoginHelper} from "./login/SSOLoginHelper.js";
|
||||||
|
|
||||||
|
function normalizeHomeserver(homeServer) {
|
||||||
|
try {
|
||||||
|
return new URL(homeServer).origin;
|
||||||
|
} catch (err) {
|
||||||
|
return new URL(`https://${homeServer}`).origin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const LoadStatus = createEnum(
|
export const LoadStatus = createEnum(
|
||||||
"NotLoading",
|
"NotLoading",
|
||||||
|
@ -42,14 +53,6 @@ export const LoginFailure = createEnum(
|
||||||
"Unknown",
|
"Unknown",
|
||||||
);
|
);
|
||||||
|
|
||||||
function normalizeHomeserver(homeServer) {
|
|
||||||
try {
|
|
||||||
return new URL(homeServer).origin;
|
|
||||||
} catch (err) {
|
|
||||||
return new URL(`https://${homeServer}`).origin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SessionContainer {
|
export class SessionContainer {
|
||||||
constructor({platform, olmPromise, workerPromise}) {
|
constructor({platform, olmPromise, workerPromise}) {
|
||||||
this._platform = platform;
|
this._platform = platform;
|
||||||
|
@ -97,25 +100,56 @@ export class SessionContainer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async startWithLogin(homeServer, username, password) {
|
_parseLoginOptions(options, homeServer) {
|
||||||
if (this._status.get() !== LoadStatus.NotLoading) {
|
/*
|
||||||
|
Take server response and return new object which has two props password and sso which
|
||||||
|
implements LoginMethod
|
||||||
|
*/
|
||||||
|
const flows = options.flows;
|
||||||
|
const result = {};
|
||||||
|
for (const flow of flows) {
|
||||||
|
if (flow.type === "m.login.password") {
|
||||||
|
result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password});
|
||||||
|
}
|
||||||
|
else if (flow.type === "m.login.sso" && flows.find(flow => flow.type === "m.login.token")) {
|
||||||
|
result.sso = new SSOLoginHelper(homeServer);
|
||||||
|
}
|
||||||
|
else if (flow.type === "m.login.token") {
|
||||||
|
result.token = loginToken => new TokenLoginMethod({homeServer, loginToken});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryLogin(homeServer) {
|
||||||
|
const normalizedHS = normalizeHomeserver(homeServer);
|
||||||
|
const hsApi = new HomeServerApi({homeServer: normalizedHS, request: this._platform.request});
|
||||||
|
const response = await hsApi.getLoginFlows().response();
|
||||||
|
return this._parseLoginOptions(response, normalizedHS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async startWithLogin(loginMethod) {
|
||||||
|
const currentStatus = this._status.get();
|
||||||
|
if (currentStatus !== LoadStatus.LoginFailed &&
|
||||||
|
currentStatus !== LoadStatus.NotLoading &&
|
||||||
|
currentStatus !== LoadStatus.Error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._resetStatus();
|
||||||
await this._platform.logger.run("login", async log => {
|
await this._platform.logger.run("login", async log => {
|
||||||
this._status.set(LoadStatus.Login);
|
this._status.set(LoadStatus.Login);
|
||||||
homeServer = normalizeHomeserver(homeServer);
|
|
||||||
const clock = this._platform.clock;
|
const clock = this._platform.clock;
|
||||||
let sessionInfo;
|
let sessionInfo;
|
||||||
try {
|
try {
|
||||||
const request = this._platform.request;
|
const request = this._platform.request;
|
||||||
const hsApi = new HomeServerApi({homeServer, request});
|
const hsApi = new HomeServerApi({homeServer: loginMethod.homeServer, request});
|
||||||
const loginData = await hsApi.passwordLogin(username, password, "Hydrogen", {log}).response();
|
const loginData = await loginMethod.login(hsApi, "Hydrogen", log);
|
||||||
const sessionId = this.createNewSessionId();
|
const sessionId = this.createNewSessionId();
|
||||||
sessionInfo = {
|
sessionInfo = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
deviceId: loginData.device_id,
|
deviceId: loginData.device_id,
|
||||||
userId: loginData.user_id,
|
userId: loginData.user_id,
|
||||||
homeServer: homeServer,
|
homeServer: loginMethod.homeServer,
|
||||||
accessToken: loginData.access_token,
|
accessToken: loginData.access_token,
|
||||||
lastUsed: clock.now()
|
lastUsed: clock.now()
|
||||||
};
|
};
|
||||||
|
@ -270,6 +304,10 @@ export class SessionContainer {
|
||||||
return this._error;
|
return this._error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get loginFailure() {
|
||||||
|
return this._loginFailure;
|
||||||
|
}
|
||||||
|
|
||||||
/** only set at loadStatus InitialSync, CatchupSync or Ready */
|
/** only set at loadStatus InitialSync, CatchupSync or Ready */
|
||||||
get sync() {
|
get sync() {
|
||||||
return this._sync;
|
return this._sync;
|
||||||
|
@ -319,4 +357,10 @@ export class SessionContainer {
|
||||||
this._sessionId = null;
|
this._sessionId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resetStatus() {
|
||||||
|
this._status.set(LoadStatus.NotLoading);
|
||||||
|
this._error = null;
|
||||||
|
this._loginFailure = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
30
src/matrix/login/LoginMethod.js
Normal file
30
src/matrix/login/LoginMethod.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class LoginMethod {
|
||||||
|
constructor({homeServer}) {
|
||||||
|
this.homeServer = homeServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
async login(hsApi, deviceName, log) {
|
||||||
|
/*
|
||||||
|
Regardless of the login method, SessionContainer.startWithLogin()
|
||||||
|
can do SomeLoginMethod.login()
|
||||||
|
*/
|
||||||
|
throw("Not Implemented");
|
||||||
|
}
|
||||||
|
}
|
29
src/matrix/login/PasswordLoginMethod.js
Normal file
29
src/matrix/login/PasswordLoginMethod.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
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 {LoginMethod} from "./LoginMethod.js";
|
||||||
|
|
||||||
|
export class PasswordLoginMethod extends LoginMethod {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this.username = options.username;
|
||||||
|
this.password = options.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(hsApi, deviceName, log) {
|
||||||
|
return await hsApi.passwordLogin(this.username, this.password, deviceName, {log}).response();
|
||||||
|
}
|
||||||
|
}
|
27
src/matrix/login/SSOLoginHelper.js
Normal file
27
src/matrix/login/SSOLoginHelper.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SSOLoginHelper{
|
||||||
|
constructor(homeserver) {
|
||||||
|
this._homeserver = homeserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
get homeserver() { return this._homeserver; }
|
||||||
|
|
||||||
|
createSSORedirectURL(returnURL) {
|
||||||
|
return `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${returnURL}`;
|
||||||
|
}
|
||||||
|
}
|
29
src/matrix/login/TokenLoginMethod.js
Normal file
29
src/matrix/login/TokenLoginMethod.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
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 {LoginMethod} from "./LoginMethod.js";
|
||||||
|
import {makeTxnId} from "../common.js";
|
||||||
|
|
||||||
|
export class TokenLoginMethod extends LoginMethod {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this._loginToken = options.loginToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(hsApi, deviceName, log) {
|
||||||
|
return await hsApi.tokenLogin(this._loginToken, makeTxnId(), deviceName, {log}).response();
|
||||||
|
}
|
||||||
|
}
|
|
@ -134,6 +134,10 @@ export class HomeServerApi {
|
||||||
return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options);
|
return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLoginFlows() {
|
||||||
|
return this._unauthedRequest("GET", this._url("/login"), null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
passwordLogin(username, password, initialDeviceDisplayName, options = null) {
|
passwordLogin(username, password, initialDeviceDisplayName, options = null) {
|
||||||
return this._unauthedRequest("POST", this._url("/login"), null, {
|
return this._unauthedRequest("POST", this._url("/login"), null, {
|
||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
|
@ -146,6 +150,18 @@ export class HomeServerApi {
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokenLogin(loginToken, txnId, initialDeviceDisplayName, options = null) {
|
||||||
|
return this._unauthedRequest("POST", this._url("/login"), null, {
|
||||||
|
"type": "m.login.token",
|
||||||
|
"identifier": {
|
||||||
|
"type": "m.id.user",
|
||||||
|
},
|
||||||
|
"token": loginToken,
|
||||||
|
"txn_id": txnId,
|
||||||
|
"initial_device_display_name": initialDeviceDisplayName
|
||||||
|
}, options);
|
||||||
|
}
|
||||||
|
|
||||||
createFilter(userId, filter, options = null) {
|
createFilter(userId, filter, options = null) {
|
||||||
return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options);
|
return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,6 +240,10 @@ export class Platform {
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openUrl(url) {
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
parseHTML(html) {
|
parseHTML(html) {
|
||||||
return parseHTML(html);
|
return parseHTML(html);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,14 @@ export class History extends BaseObservableValue {
|
||||||
}
|
}
|
||||||
|
|
||||||
get() {
|
get() {
|
||||||
|
/*
|
||||||
|
All URLS in Hydrogen will use <root>/#/segment/value/...
|
||||||
|
But for SSO, we need to handle <root>/?loginToken=<TOKEN>
|
||||||
|
Handle that as a special case for now.
|
||||||
|
*/
|
||||||
|
if (document.location.search.includes("loginToken")) {
|
||||||
|
return document.location.search;
|
||||||
|
}
|
||||||
return document.location.hash;
|
return document.location.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,14 @@ export class SettingsStorage {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setString(key, value) {
|
||||||
|
this._set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getString(key) {
|
||||||
|
return window.localStorage.getItem(`${this._prefix}${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
async remove(key) {
|
async remove(key) {
|
||||||
window.localStorage.removeItem(`${this._prefix}${key}`);
|
window.localStorage.removeItem(`${this._prefix}${key}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ limitations under the License.
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionPickerView .session-info > :not(:first-child) {
|
.SessionPickerView .session-info> :not(:first-child) {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,19 +50,19 @@ limitations under the License.
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoginView {
|
.PasswordLoginView {
|
||||||
padding: 0.4em;
|
padding: 0 0.4em 0.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionLoadStatusView {
|
.SessionLoadStatusView, .LoginView_query-spinner {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionLoadStatusView > :not(:first-child) {
|
.SessionLoadStatusView> :not(:first-child), .LoginView_query-spinner> :not(:first-child) {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionLoadStatusView p {
|
.SessionLoadStatusView p, .LoginView_query-spinner p {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -70,3 +70,29 @@ limitations under the License.
|
||||||
.SessionLoadStatusView .spinner {
|
.SessionLoadStatusView .spinner {
|
||||||
--size: 20px;
|
--size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.StartSSOLoginView {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 0.4em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.StartSSOLoginView_button {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoginView_separator {
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CompleteSSOView_title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoginView_sso {
|
||||||
|
padding: 0.4em 0.4em 0;
|
||||||
|
}
|
||||||
|
|
|
@ -223,6 +223,25 @@ a.button-action {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.StartSSOLoginView_button {
|
||||||
|
border: 1px solid #03B381;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoginView_back {
|
||||||
|
background-image: url("./icons/chevron-left.svg");
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoginView_separator {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CompleteSSOView_title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 600px) {
|
@media screen and (min-width: 600px) {
|
||||||
.PreSessionScreen {
|
.PreSessionScreen {
|
||||||
box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
|
30
src/platform/web/ui/login/CompleteSSOView.js
Normal file
30
src/platform/web/ui/login/CompleteSSOView.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
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 {TemplateView} from "../general/TemplateView.js";
|
||||||
|
import {SessionLoadStatusView} from "./SessionLoadStatusView.js";
|
||||||
|
|
||||||
|
export class CompleteSSOView extends TemplateView {
|
||||||
|
render(t) {
|
||||||
|
return t.div({ className: "CompleteSSOView" },
|
||||||
|
[
|
||||||
|
t.p({ className: "CompleteSSOView_title" }, "Finishing up your SSO Login"),
|
||||||
|
t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))),
|
||||||
|
t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,61 +16,58 @@ limitations under the License.
|
||||||
|
|
||||||
import {TemplateView} from "../general/TemplateView.js";
|
import {TemplateView} from "../general/TemplateView.js";
|
||||||
import {hydrogenGithubLink} from "./common.js";
|
import {hydrogenGithubLink} from "./common.js";
|
||||||
|
import {PasswordLoginView} from "./PasswordLoginView.js";
|
||||||
|
import {CompleteSSOView} from "./CompleteSSOView.js";
|
||||||
import {SessionLoadStatusView} from "./SessionLoadStatusView.js";
|
import {SessionLoadStatusView} from "./SessionLoadStatusView.js";
|
||||||
|
import {spinner} from "../common.js";
|
||||||
|
|
||||||
export class LoginView extends TemplateView {
|
export class LoginView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
const disabled = vm => !!vm.isBusy;
|
const disabled = vm => vm.isBusy;
|
||||||
const username = t.input({
|
|
||||||
id: "username",
|
|
||||||
type: "text",
|
|
||||||
placeholder: vm.i18n`Username`,
|
|
||||||
disabled
|
|
||||||
});
|
|
||||||
const password = t.input({
|
|
||||||
id: "password",
|
|
||||||
type: "password",
|
|
||||||
placeholder: vm.i18n`Password`,
|
|
||||||
disabled
|
|
||||||
});
|
|
||||||
const homeserver = t.input({
|
|
||||||
id: "homeserver",
|
|
||||||
type: "text",
|
|
||||||
placeholder: vm.i18n`Your matrix homeserver`,
|
|
||||||
value: vm.defaultHomeServer,
|
|
||||||
disabled
|
|
||||||
});
|
|
||||||
|
|
||||||
return t.div({className: "PreSessionScreen"}, [
|
return t.div({className: "PreSessionScreen"}, [
|
||||||
|
t.button({
|
||||||
|
className: "button-utility LoginView_back",
|
||||||
|
onClick: () => vm.goBack(),
|
||||||
|
disabled
|
||||||
|
}),
|
||||||
t.div({className: "logo"}),
|
t.div({className: "logo"}),
|
||||||
t.div({className: "LoginView form"}, [
|
t.h1([vm.i18n`Sign In`]),
|
||||||
t.h1([vm.i18n`Sign In`]),
|
t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null),
|
||||||
t.if(vm => vm.error, t => t.div({className: "error"}, vm => vm.error)),
|
t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" },
|
||||||
t.form({
|
[
|
||||||
onSubmit: evnt => {
|
t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))),
|
||||||
evnt.preventDefault();
|
t.label({for: "homeserver"}, vm.i18n`Homeserver`),
|
||||||
vm.login(username.value, password.value, homeserver.value);
|
t.input({
|
||||||
}
|
id: "homeserver",
|
||||||
}, [
|
type: "text",
|
||||||
t.div({className: "form-row"}, [t.label({for: "username"}, vm.i18n`Username`), username]),
|
placeholder: vm.i18n`Your matrix homeserver`,
|
||||||
t.div({className: "form-row"}, [t.label({for: "password"}, vm.i18n`Password`), password]),
|
value: vm.homeserver,
|
||||||
t.div({className: "form-row"}, [t.label({for: "homeserver"}, vm.i18n`Homeserver`), homeserver]),
|
disabled,
|
||||||
t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null),
|
onChange: event => vm.updateHomeServer(event.target.value),
|
||||||
t.div({className: "button-row"}, [
|
})
|
||||||
t.a({
|
]
|
||||||
className: "button-action secondary",
|
)),
|
||||||
href: vm.cancelUrl
|
t.if(vm => vm.isFetchingLoginOptions, t => t.div({className: "LoginView_query-spinner"}, [spinner(t), t.p("Fetching available login options...")])),
|
||||||
}, [vm.i18n`Go Back`]),
|
t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null),
|
||||||
t.button({
|
t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)),
|
||||||
className: "button-action primary",
|
t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null),
|
||||||
type: "submit"
|
t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null),
|
||||||
}, vm.i18n`Log In`),
|
// use t.mapView rather than t.if to create a new view when the view model changes too
|
||||||
]),
|
t.p(hydrogenGithubLink(t))
|
||||||
]),
|
|
||||||
// use t.mapView rather than t.if to create a new view when the view model changes too
|
|
||||||
t.p(hydrogenGithubLink(t))
|
|
||||||
])
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StartSSOLoginView extends TemplateView {
|
||||||
|
render(t, vm) {
|
||||||
|
return t.div({ className: "StartSSOLoginView" },
|
||||||
|
t.button({
|
||||||
|
className: "StartSSOLoginView_button button-action secondary",
|
||||||
|
type: "button",
|
||||||
|
onClick: () => vm.startSSOLogin(),
|
||||||
|
disabled: vm => vm.isBusy
|
||||||
|
}, vm.i18n`Log in with SSO`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
57
src/platform/web/ui/login/PasswordLoginView.js
Normal file
57
src/platform/web/ui/login/PasswordLoginView.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
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 {TemplateView} from "../general/TemplateView.js";
|
||||||
|
|
||||||
|
export class PasswordLoginView extends TemplateView {
|
||||||
|
render(t, vm) {
|
||||||
|
const disabled = vm => !!vm.isBusy;
|
||||||
|
const username = t.input({
|
||||||
|
id: "username",
|
||||||
|
type: "text",
|
||||||
|
placeholder: vm.i18n`Username`,
|
||||||
|
disabled
|
||||||
|
});
|
||||||
|
const password = t.input({
|
||||||
|
id: "password",
|
||||||
|
type: "password",
|
||||||
|
placeholder: vm.i18n`Password`,
|
||||||
|
disabled
|
||||||
|
});
|
||||||
|
|
||||||
|
return t.div({className: "PasswordLoginView form"}, [
|
||||||
|
t.if(vm => vm.error, t => t.div({ className: "error" }, vm => vm.error)),
|
||||||
|
t.form({
|
||||||
|
onSubmit: evnt => {
|
||||||
|
evnt.preventDefault();
|
||||||
|
vm.login(username.value, password.value);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))),
|
||||||
|
t.div({ className: "form-row" }, [t.label({ for: "username" }, vm.i18n`Username`), username]),
|
||||||
|
t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]),
|
||||||
|
t.div({ className: "button-row" }, [
|
||||||
|
t.button({
|
||||||
|
className: "button-action primary",
|
||||||
|
type: "submit",
|
||||||
|
disabled
|
||||||
|
}, vm.i18n`Log In`),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Reference in a new issue