Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Support social login & password on soft logout page (#7879)
Browse files Browse the repository at this point in the history
* Code style: Modernize

* Make Soft Logout page support Social Sign On

Fixes element-hq/element-web#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
  • Loading branch information
turt2live authored Feb 23, 2022
1 parent 1830de2 commit d71922c
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 75 deletions.
185 changes: 111 additions & 74 deletions src/components/structures/auth/SoftLogout.tsx
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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';
Expand All @@ -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 {
Expand All @@ -60,7 +62,7 @@ interface IProps {
}

interface IState {
loginView: number;
loginView: LoginView;
keyBackupNeeded: boolean;
busy: boolean;
password: string;
Expand All @@ -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: "",
Expand All @@ -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" });
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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();

Expand Down Expand Up @@ -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);
Expand All @@ -194,20 +198,70 @@ 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;
}

Lifecycle.hydrateSession(credentials).then(() => {
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 />;
}

Expand All @@ -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 (
<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>
);
return this.renderSsoForm(introText);
}

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
Expand All @@ -287,7 +324,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
);
}

render() {
public render() {
return (
<AuthPage>
<AuthHeader />
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit d71922c

Please sign in to comment.