forked from mystiq/hydrogen-web
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 {SessionLoadViewModel} from "./SessionLoadViewModel.js";
|
||||
import {LoginViewModel} from "./LoginViewModel.js";
|
||||
import {LoginViewModel} from "./login/LoginViewModel.js";
|
||||
import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
|
||||
import {ViewModel} from "./ViewModel.js";
|
||||
|
||||
|
@ -35,12 +35,14 @@ export class RootViewModel extends ViewModel {
|
|||
async load() {
|
||||
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._applyNavigation(true);
|
||||
}
|
||||
|
||||
async _applyNavigation(shouldRestoreLastUrl) {
|
||||
const isLogin = this.navigation.observe("login").get();
|
||||
const sessionId = this.navigation.observe("session").get();
|
||||
const isLogin = this.navigation.path.get("login")
|
||||
const sessionId = this.navigation.path.get("session")?.value;
|
||||
const loginToken = this.navigation.path.get("sso")?.value;
|
||||
if (isLogin) {
|
||||
if (this.activeSection !== "login") {
|
||||
this._showLogin();
|
||||
|
@ -65,7 +67,13 @@ export class RootViewModel extends ViewModel {
|
|||
this._showSessionLoader(sessionId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (loginToken) {
|
||||
this.urlCreator.normalizeUrl();
|
||||
if (this.activeSection !== "login") {
|
||||
this._showLogin(loginToken);
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
if (!(shouldRestoreLastUrl && this.urlCreator.tryRestoreLastUrl())) {
|
||||
const sessionInfos = await this.platform.sessionInfoStorage.getAll();
|
||||
|
@ -94,7 +102,7 @@ export class RootViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
_showLogin() {
|
||||
_showLogin(loginToken) {
|
||||
this._setSection(() => {
|
||||
this._loginViewModel = new LoginViewModel(this.childOptions({
|
||||
defaultHomeServer: this.platform.config["defaultHomeServer"],
|
||||
|
@ -111,6 +119,7 @@ export class RootViewModel extends ViewModel {
|
|||
this._pendingSessionContainer = sessionContainer;
|
||||
this.navigation.push("session", sessionContainer.sessionId);
|
||||
},
|
||||
loginToken
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
@ -123,13 +132,11 @@ export class RootViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
_showSessionLoader(sessionId) {
|
||||
this._setSection(() => {
|
||||
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
||||
createAndStartSessionContainer: () => {
|
||||
const sessionContainer = this._createSessionContainer();
|
||||
sessionContainer.startWithExistingSession(sessionId);
|
||||
return sessionContainer;
|
||||
},
|
||||
this._setSection(() => {
|
||||
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
||||
sessionContainer,
|
||||
ready: sessionContainer => this._showSession(sessionContainer)
|
||||
}));
|
||||
this._sessionLoadViewModel.start();
|
||||
|
|
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js";
|
||||
import {LoadStatus} from "../matrix/SessionContainer.js";
|
||||
import {SyncStatus} from "../matrix/Sync.js";
|
||||
import {ViewModel} from "./ViewModel.js";
|
||||
|
||||
export class SessionLoadViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const {createAndStartSessionContainer, ready, homeserver, deleteSessionOnCancel} = options;
|
||||
this._createAndStartSessionContainer = createAndStartSessionContainer;
|
||||
const {sessionContainer, ready, homeserver, deleteSessionOnCancel} = options;
|
||||
this._sessionContainer = sessionContainer;
|
||||
this._ready = ready;
|
||||
this._homeserver = homeserver;
|
||||
this._deleteSessionOnCancel = deleteSessionOnCancel;
|
||||
|
@ -38,7 +38,6 @@ export class SessionLoadViewModel extends ViewModel {
|
|||
try {
|
||||
this._loading = true;
|
||||
this.emitChange("loading");
|
||||
this._sessionContainer = this._createAndStartSessionContainer();
|
||||
this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => {
|
||||
this.emitChange("loadLabel");
|
||||
// wait for initial sync, but not catchup sync
|
||||
|
@ -109,22 +108,9 @@ export class SessionLoadViewModel extends ViewModel {
|
|||
return `Something went wrong: ${error && error.message}.`;
|
||||
}
|
||||
|
||||
// Statuses related to login are handled by respective login view models
|
||||
if (sc) {
|
||||
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:
|
||||
return `Setting up your encryption keys…`;
|
||||
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}`;
|
||||
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) {
|
||||
case undefined:
|
||||
// allowed root segments
|
||||
return type === "login" || type === "session";
|
||||
return type === "login" || type === "session" || type === "sso";
|
||||
case "session":
|
||||
return type === "room" || type === "rooms" || type === "settings";
|
||||
case "rooms":
|
||||
|
@ -152,6 +152,10 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
|
|||
const userId = iterator.next().value;
|
||||
if (!userId) { break; }
|
||||
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 {
|
||||
// might be undefined, which will be turned into true by Segment
|
||||
const value = iterator.next().value;
|
||||
|
@ -181,7 +185,8 @@ export function stringifyPath(path) {
|
|||
}
|
||||
break;
|
||||
case "right-panel":
|
||||
// Ignore right-panel in url
|
||||
case "sso":
|
||||
// Do not put these segments in URL
|
||||
continue;
|
||||
default:
|
||||
urlPath += `/${segment.type}`;
|
||||
|
@ -228,6 +233,12 @@ export function tests() {
|
|||
const urlPath = stringifyPath(path);
|
||||
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 => {
|
||||
const segments = parseUrlPath("/session/1/rooms/a,b,c/3");
|
||||
assert.equal(segments.length, 3);
|
||||
|
|
|
@ -23,6 +23,17 @@ import {MediaRepository} from "./net/MediaRepository.js";
|
|||
import {RequestScheduler} from "./net/RequestScheduler.js";
|
||||
import {Sync, SyncStatus} from "./Sync.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(
|
||||
"NotLoading",
|
||||
|
@ -42,14 +53,6 @@ export const LoginFailure = createEnum(
|
|||
"Unknown",
|
||||
);
|
||||
|
||||
function normalizeHomeserver(homeServer) {
|
||||
try {
|
||||
return new URL(homeServer).origin;
|
||||
} catch (err) {
|
||||
return new URL(`https://${homeServer}`).origin;
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionContainer {
|
||||
constructor({platform, olmPromise, workerPromise}) {
|
||||
this._platform = platform;
|
||||
|
@ -97,25 +100,56 @@ export class SessionContainer {
|
|||
});
|
||||
}
|
||||
|
||||
async startWithLogin(homeServer, username, password) {
|
||||
if (this._status.get() !== LoadStatus.NotLoading) {
|
||||
_parseLoginOptions(options, homeServer) {
|
||||
/*
|
||||
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;
|
||||
}
|
||||
this._resetStatus();
|
||||
await this._platform.logger.run("login", async log => {
|
||||
this._status.set(LoadStatus.Login);
|
||||
homeServer = normalizeHomeserver(homeServer);
|
||||
const clock = this._platform.clock;
|
||||
let sessionInfo;
|
||||
try {
|
||||
const request = this._platform.request;
|
||||
const hsApi = new HomeServerApi({homeServer, request});
|
||||
const loginData = await hsApi.passwordLogin(username, password, "Hydrogen", {log}).response();
|
||||
const hsApi = new HomeServerApi({homeServer: loginMethod.homeServer, request});
|
||||
const loginData = await loginMethod.login(hsApi, "Hydrogen", log);
|
||||
const sessionId = this.createNewSessionId();
|
||||
sessionInfo = {
|
||||
id: sessionId,
|
||||
deviceId: loginData.device_id,
|
||||
userId: loginData.user_id,
|
||||
homeServer: homeServer,
|
||||
homeServer: loginMethod.homeServer,
|
||||
accessToken: loginData.access_token,
|
||||
lastUsed: clock.now()
|
||||
};
|
||||
|
@ -270,6 +304,10 @@ export class SessionContainer {
|
|||
return this._error;
|
||||
}
|
||||
|
||||
get loginFailure() {
|
||||
return this._loginFailure;
|
||||
}
|
||||
|
||||
/** only set at loadStatus InitialSync, CatchupSync or Ready */
|
||||
get sync() {
|
||||
return this._sync;
|
||||
|
@ -319,4 +357,10 @@ export class SessionContainer {
|
|||
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);
|
||||
}
|
||||
|
||||
getLoginFlows() {
|
||||
return this._unauthedRequest("GET", this._url("/login"), null, null, null);
|
||||
}
|
||||
|
||||
passwordLogin(username, password, initialDeviceDisplayName, options = null) {
|
||||
return this._unauthedRequest("POST", this._url("/login"), null, {
|
||||
"type": "m.login.password",
|
||||
|
@ -146,6 +150,18 @@ export class HomeServerApi {
|
|||
}, 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) {
|
||||
return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options);
|
||||
}
|
||||
|
|
|
@ -240,6 +240,10 @@ export class Platform {
|
|||
return promise;
|
||||
}
|
||||
|
||||
openUrl(url) {
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
parseHTML(html) {
|
||||
return parseHTML(html);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,14 @@ export class History extends BaseObservableValue {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,14 @@ export class SettingsStorage {
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
async setString(key, value) {
|
||||
this._set(key, value);
|
||||
}
|
||||
|
||||
async getString(key) {
|
||||
return window.localStorage.getItem(`${this._prefix}${key}`);
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
window.localStorage.removeItem(`${this._prefix}${key}`);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ limitations under the License.
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.SessionPickerView .session-info > :not(:first-child) {
|
||||
.SessionPickerView .session-info> :not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
|
@ -50,19 +50,19 @@ limitations under the License.
|
|||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.LoginView {
|
||||
padding: 0.4em;
|
||||
.PasswordLoginView {
|
||||
padding: 0 0.4em 0.4em;
|
||||
}
|
||||
|
||||
.SessionLoadStatusView {
|
||||
.SessionLoadStatusView, .LoginView_query-spinner {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.SessionLoadStatusView > :not(:first-child) {
|
||||
.SessionLoadStatusView> :not(:first-child), .LoginView_query-spinner> :not(:first-child) {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.SessionLoadStatusView p {
|
||||
.SessionLoadStatusView p, .LoginView_query-spinner p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -70,3 +70,29 @@ limitations under the License.
|
|||
.SessionLoadStatusView .spinner {
|
||||
--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;
|
||||
}
|
||||
|
||||
.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) {
|
||||
.PreSessionScreen {
|
||||
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 {hydrogenGithubLink} from "./common.js";
|
||||
import {PasswordLoginView} from "./PasswordLoginView.js";
|
||||
import {CompleteSSOView} from "./CompleteSSOView.js";
|
||||
import {SessionLoadStatusView} from "./SessionLoadStatusView.js";
|
||||
import {spinner} from "../common.js";
|
||||
|
||||
export class LoginView extends TemplateView {
|
||||
render(t, vm) {
|
||||
const disabled = vm => !!vm.isBusy;
|
||||
const username = t.input({
|
||||
id: "username",
|
||||
type: "text",
|
||||
placeholder: vm.i18n`Username`,
|
||||
const disabled = vm => vm.isBusy;
|
||||
|
||||
return t.div({className: "PreSessionScreen"}, [
|
||||
t.button({
|
||||
className: "button-utility LoginView_back",
|
||||
onClick: () => vm.goBack(),
|
||||
disabled
|
||||
});
|
||||
const password = t.input({
|
||||
id: "password",
|
||||
type: "password",
|
||||
placeholder: vm.i18n`Password`,
|
||||
disabled
|
||||
});
|
||||
const homeserver = t.input({
|
||||
}),
|
||||
t.div({className: "logo"}),
|
||||
t.h1([vm.i18n`Sign In`]),
|
||||
t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null),
|
||||
t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" },
|
||||
[
|
||||
t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))),
|
||||
t.label({for: "homeserver"}, vm.i18n`Homeserver`),
|
||||
t.input({
|
||||
id: "homeserver",
|
||||
type: "text",
|
||||
placeholder: vm.i18n`Your matrix homeserver`,
|
||||
value: vm.defaultHomeServer,
|
||||
disabled
|
||||
});
|
||||
|
||||
return t.div({className: "PreSessionScreen"}, [
|
||||
t.div({className: "logo"}),
|
||||
t.div({className: "LoginView form"}, [
|
||||
t.h1([vm.i18n`Sign In`]),
|
||||
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, homeserver.value);
|
||||
}
|
||||
}, [
|
||||
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: "form-row"}, [t.label({for: "homeserver"}, vm.i18n`Homeserver`), homeserver]),
|
||||
value: vm.homeserver,
|
||||
disabled,
|
||||
onChange: event => vm.updateHomeServer(event.target.value),
|
||||
})
|
||||
]
|
||||
)),
|
||||
t.if(vm => vm.isFetchingLoginOptions, t => t.div({className: "LoginView_query-spinner"}, [spinner(t), t.p("Fetching available login options...")])),
|
||||
t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null),
|
||||
t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)),
|
||||
t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null),
|
||||
t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null),
|
||||
t.div({className: "button-row"}, [
|
||||
t.a({
|
||||
className: "button-action secondary",
|
||||
href: vm.cancelUrl
|
||||
}, [vm.i18n`Go Back`]),
|
||||
t.button({
|
||||
className: "button-action primary",
|
||||
type: "submit"
|
||||
}, 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))
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
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`),
|
||||
]),
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue