Support social login & password on soft logout page (#7879)

* Code style: Modernize

* Make Soft Logout page support Social Sign On

Fixes https://github.com/vector-im/element-web/issues/21099

This commit does a few things:
* Moves rendering of the flows to functions
* Adds a new login view enum for Password + SSO (mirroring logic from registration)
* Makes an absolute mess of the resulting diff

* Lint & i18n

* Remove spurious typing
This commit is contained in:
Travis Ralston 2022-02-23 09:22:37 -07:00 committed by GitHub
parent 1830de2733
commit d71922ca51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 111 additions and 74 deletions

View File

@ -1,5 +1,5 @@
/*
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Copyright 2019-2022 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.
@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk";
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
@ -34,18 +35,19 @@ import Spinner from "../../views/elements/Spinner";
import AuthHeader from "../../views/auth/AuthHeader";
import AuthBody from "../../views/auth/AuthBody";
const LOGIN_VIEW = {
LOADING: 1,
PASSWORD: 2,
CAS: 3, // SSO, but old
SSO: 4,
UNSUPPORTED: 5,
};
enum LoginView {
Loading,
Password,
CAS, // SSO, but old
SSO,
PasswordWithSocialSignOn,
Unsupported,
}
const FLOWS_TO_VIEWS = {
"m.login.password": LOGIN_VIEW.PASSWORD,
"m.login.cas": LOGIN_VIEW.CAS,
"m.login.sso": LOGIN_VIEW.SSO,
const STATIC_FLOWS_TO_VIEWS = {
"m.login.password": LoginView.Password,
"m.login.cas": LoginView.CAS,
"m.login.sso": LoginView.SSO,
};
interface IProps {
@ -60,7 +62,7 @@ interface IProps {
}
interface IState {
loginView: number;
loginView: LoginView;
keyBackupNeeded: boolean;
busy: boolean;
password: string;
@ -70,11 +72,11 @@ interface IState {
@replaceableComponent("structures.auth.SoftLogout")
export default class SoftLogout extends React.Component<IProps, IState> {
constructor(props) {
public constructor(props: IProps) {
super(props);
this.state = {
loginView: LOGIN_VIEW.LOADING,
loginView: LoginView.Loading,
keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount)
busy: false,
password: "",
@ -83,7 +85,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
};
}
componentDidMount(): void {
public componentDidMount(): void {
// We've ended up here when we don't need to - navigate to login
if (!Lifecycle.isSoftLogout()) {
dis.dispatch({ action: "start_login" });
@ -100,7 +102,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
}
}
onClearAll = () => {
private onClearAll = () => {
Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, {
onFinished: (wipeData) => {
if (!wipeData) return;
@ -115,7 +117,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
const queryParams = this.props.realQueryParams;
const hasAllParams = queryParams && queryParams['loginToken'];
if (hasAllParams) {
this.setState({ loginView: LOGIN_VIEW.LOADING });
this.setState({ loginView: LoginView.Loading });
this.trySsoLogin();
return;
}
@ -124,21 +126,23 @@ export default class SoftLogout extends React.Component<IProps, IState> {
// care about login flows here, unless it is the single flow we support.
const client = MatrixClientPeg.get();
const flows = (await client.loginFlows()).flows;
const loginViews = flows.map(f => FLOWS_TO_VIEWS[f.type]);
const loginViews = flows.map(f => STATIC_FLOWS_TO_VIEWS[f.type]);
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
const isSocialSignOn = loginViews.includes(LoginView.Password) && loginViews.includes(LoginView.SSO);
const firstView = loginViews.filter(f => !!f)[0] || LoginView.Unsupported;
const chosenView = isSocialSignOn ? LoginView.PasswordWithSocialSignOn : firstView;
this.setState({ flows, loginView: chosenView });
}
onPasswordChange = (ev) => {
private onPasswordChange = (ev) => {
this.setState({ password: ev.target.value });
};
onForgotPassword = () => {
private onForgotPassword = () => {
dis.dispatch({ action: 'start_password_recovery' });
};
onPasswordLogin = async (ev) => {
private onPasswordLogin = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
@ -178,7 +182,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
});
};
async trySsoLogin() {
private async trySsoLogin() {
this.setState({ busy: true });
const hsUrl = localStorage.getItem(SSO_HOMESERVER_URL_KEY);
@ -194,7 +198,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams);
} catch (e) {
logger.error(e);
this.setState({ busy: false, loginView: LOGIN_VIEW.UNSUPPORTED });
this.setState({ busy: false, loginView: LoginView.Unsupported });
return;
}
@ -202,12 +206,62 @@ export default class SoftLogout extends React.Component<IProps, IState> {
if (this.props.onTokenLoginCompleted) this.props.onTokenLoginCompleted();
}).catch((e) => {
logger.error(e);
this.setState({ busy: false, loginView: LOGIN_VIEW.UNSUPPORTED });
this.setState({ busy: false, loginView: LoginView.Unsupported });
});
}
private renderPasswordForm(introText: Optional<string>): JSX.Element {
let error: JSX.Element = null;
if (this.state.errorText) {
error = <span className='mx_Login_error'>{ this.state.errorText }</span>;
}
return (
<form onSubmit={this.onPasswordLogin}>
{ introText ? <p>{ introText }</p> : null }
{ error }
<Field
type="password"
label={_t("Password")}
onChange={this.onPasswordChange}
value={this.state.password}
disabled={this.state.busy}
/>
<AccessibleButton
onClick={this.onPasswordLogin}
kind="primary"
type="submit"
disabled={this.state.busy}
>
{ _t("Sign In") }
</AccessibleButton>
<AccessibleButton onClick={this.onForgotPassword} kind="link">
{ _t("Forgotten your password?") }
</AccessibleButton>
</form>
);
}
private renderSsoForm(introText: Optional<string>): JSX.Element {
const loginType = this.state.loginView === LoginView.CAS ? "cas" : "sso";
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
return (
<div>
{ introText ? <p>{ introText }</p> : null }
<SSOButtons
matrixClient={MatrixClientPeg.get()}
flow={flow}
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
/>
</div>
);
}
private renderSignInSection() {
if (this.state.loginView === LOGIN_VIEW.LOADING) {
if (this.state.loginView === LoginView.Loading) {
return <Spinner />;
}
@ -218,62 +272,45 @@ export default class SoftLogout extends React.Component<IProps, IState> {
"Without them, you won't be able to read all of your secure messages in any session.");
}
if (this.state.loginView === LOGIN_VIEW.PASSWORD) {
let error = null;
if (this.state.errorText) {
error = <span className='mx_Login_error'>{ this.state.errorText }</span>;
}
if (this.state.loginView === LoginView.Password) {
if (!introText) {
introText = _t("Enter your password to sign in and regain access to your account.");
} // else we already have a message and should use it (key backup warning)
return (
<form onSubmit={this.onPasswordLogin}>
<p>{ introText }</p>
{ error }
<Field
type="password"
label={_t("Password")}
onChange={this.onPasswordChange}
value={this.state.password}
disabled={this.state.busy}
/>
<AccessibleButton
onClick={this.onPasswordLogin}
kind="primary"
type="submit"
disabled={this.state.busy}
>
{ _t("Sign In") }
</AccessibleButton>
<AccessibleButton onClick={this.onForgotPassword} kind="link">
{ _t("Forgotten your password?") }
</AccessibleButton>
</form>
);
return this.renderPasswordForm(introText);
}
if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) {
if (this.state.loginView === LoginView.SSO || this.state.loginView === LoginView.CAS) {
if (!introText) {
introText = _t("Sign in and regain access to your account.");
} // else we already have a message and should use it (key backup warning)
const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso";
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
return this.renderSsoForm(introText);
}
return (
<div>
<p>{ introText }</p>
<SSOButtons
matrixClient={MatrixClientPeg.get()}
flow={flow}
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
/>
</div>
);
if (this.state.loginView === LoginView.PasswordWithSocialSignOn) {
if (!introText) {
introText = _t("Sign in and regain access to your account.");
}
// We render both forms with no intro/error to ensure the layout looks reasonably
// okay enough.
//
// Note: "mx_AuthBody_centered" text taken from registration page.
return <>
<p>{ introText }</p>
{ this.renderSsoForm(null) }
<h3 className="mx_AuthBody_centered">
{ _t(
"%(ssoButtons)s Or %(usernamePassword)s",
{
ssoButtons: "",
usernamePassword: "",
},
).trim() }
</h3>
{ this.renderPasswordForm(null) }
</>;
}
// Default: assume unsupported/error
@ -287,7 +324,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
);
}
render() {
public render() {
return (
<AuthPage>
<AuthHeader />

View File

@ -3286,9 +3286,9 @@
"Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem",
"Incorrect password": "Incorrect password",
"Failed to re-authenticate": "Failed to re-authenticate",
"Forgotten your password?": "Forgotten your password?",
"Regain access to your account and recover encryption keys stored in this session. Without them, you won't be able to read all of your secure messages in any session.": "Regain access to your account and recover encryption keys stored in this session. Without them, you won't be able to read all of your secure messages in any session.",
"Enter your password to sign in and regain access to your account.": "Enter your password to sign in and regain access to your account.",
"Forgotten your password?": "Forgotten your password?",
"Sign in and regain access to your account.": "Sign in and regain access to your account.",
"You cannot sign in to your account. Please contact your homeserver admin for more information.": "You cannot sign in to your account. Please contact your homeserver admin for more information.",
"You're signed out": "You're signed out",