Initial support for MSC2858

This commit is contained in:
Michael Telatynski 2020-11-23 17:10:15 +00:00
parent 613710b75c
commit 1d53a5cf23
10 changed files with 237 additions and 154 deletions

View file

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ComponentProps, ReactNode} from 'react';
import React, {ReactNode} from 'react';
import {MatrixError} from "matrix-js-sdk/src/http-api";
import {_t, _td} from '../../../languageHandler';
import * as sdk from '../../../index';
import Login from '../../../Login';
import Login, {ISSOFlow, LoginFlow} from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames";
import AuthPage from "../../views/auth/AuthPage";
import SSOButton from "../../views/elements/SSOButton";
import PlatformPeg from '../../../PlatformPeg';
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
@ -35,6 +35,7 @@ import PasswordLogin from "../../views/auth/PasswordLogin";
import SignInToText from "../../views/auth/SignInToText";
import InlineSpinner from "../../views/elements/InlineSpinner";
import Spinner from "../../views/elements/Spinner";
import SSOButtons from "../../views/elements/SSOButtons";
// Enable phases for login
const PHASES_ENABLED = true;
@ -90,17 +91,14 @@ interface IState {
// can we attempt to log in or are there validation errors?
canTryLogin: boolean;
phase: Phase;
flows?: LoginFlow[];
// used for preserving form values when changing homeserver
username: string;
phoneCountry?: string;
phoneNumber: string;
// Phase of the overall login dialog.
phase: Phase;
// The current login flow, such as password, SSO, etc.
// we need to load the flows from the server
currentFlow?: string;
// We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so
// that we can render it differently, and override any other error the user may
@ -113,9 +111,10 @@ interface IState {
/*
* A wire component which glues together login UI components and Login logic
*/
export default class LoginComponent extends React.Component<IProps, IState> {
export default class LoginComponent extends React.PureComponent<IProps, IState> {
private unmounted = false;
private loginLogic: Login;
private readonly stepRendererMap: Record<string, () => ReactNode>;
constructor(props) {
@ -127,11 +126,14 @@ export default class LoginComponent extends React.Component<IProps, IState> {
errorText: null,
loginIncorrect: false,
canTryLogin: true,
phase: Phase.Login,
flows: null,
username: "",
phoneCountry: null,
phoneNumber: "",
phase: Phase.Login,
currentFlow: null,
serverIsAlive: true,
serverErrorIsFatal: false,
serverDeadError: "",
@ -351,13 +353,14 @@ export default class LoginComponent extends React.Component<IProps, IState> {
};
onTryRegisterClick = ev => {
const step = this.getCurrentFlowStep();
if (step === 'm.login.sso' || step === 'm.login.cas') {
const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password");
if (!hasPasswordFlow) {
// If we're showing SSO it means that registration is also probably disabled,
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
ev.preventDefault();
ev.stopPropagation();
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
const step = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
const ssoKind = step.type === 'm.login.sso' ? 'sso' : 'cas';
PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind,
this.props.fragmentAfterLogin);
} else {
@ -397,7 +400,6 @@ export default class LoginComponent extends React.Component<IProps, IState> {
this.setState({
busy: true,
currentFlow: null, // reset flow
loginIncorrect: false,
});
@ -432,27 +434,18 @@ export default class LoginComponent extends React.Component<IProps, IState> {
loginLogic.getFlows().then((flows) => {
// look for a flow where we understand all of the steps.
for (let i = 0; i < flows.length; i++ ) {
if (!this.isSupportedFlow(flows[i])) {
continue;
}
const supportedFlows = flows.filter(this.isSupportedFlow);
// we just pick the first flow where we support all the
// steps. (we don't have a UI for multiple logins so let's skip
// that for now).
loginLogic.chooseFlow(i);
if (supportedFlows.length > 0) {
this.setState({
currentFlow: this.getCurrentFlowStep(),
});
return;
}
// we got to the end of the list without finding a suitable
// flow.
// we got to the end of the list without finding a suitable flow.
this.setState({
errorText: _t(
"This homeserver doesn't offer any login flows which are " +
"supported by this client.",
),
errorText: _t("This homeserver doesn't offer any login flows which are supported by this client."),
});
}, (err) => {
this.setState({
@ -467,7 +460,7 @@ export default class LoginComponent extends React.Component<IProps, IState> {
});
}
private isSupportedFlow(flow) {
private isSupportedFlow = (flow: LoginFlow): boolean => {
// technically the flow can have multiple steps, but no one does this
// for login and loginLogic doesn't support it so we can ignore it.
if (!this.stepRendererMap[flow.type]) {
@ -475,13 +468,9 @@ export default class LoginComponent extends React.Component<IProps, IState> {
return false;
}
return true;
}
};
private getCurrentFlowStep() {
return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null;
}
private errorTextFromError(err) {
private errorTextFromError(err: MatrixError): ReactNode {
let errCode = err.errcode;
if (!errCode && err.httpStatus) {
errCode = "HTTP " + err.httpStatus;
@ -550,37 +539,38 @@ export default class LoginComponent extends React.Component<IProps, IState> {
/>;
}
private renderLoginComponentForStep() {
if (PHASES_ENABLED && this.state.phase !== Phase.Login) {
return null;
}
renderLoginComponentForFlows() {
if (!this.state.flows) return null;
const step = this.state.currentFlow;
// this is the ideal order we want to show the flows in
const order = [
"m.login.password",
"m.login.sso",
];
if (!step) {
return null;
}
const stepRenderer = this.stepRendererMap[step];
if (stepRenderer) {
return stepRenderer();
}
return null;
}
private renderPasswordStep = () => {
let onEditServerDetailsClick = null;
// If custom URLs are allowed, wire up the server details edit link.
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
if (!SdkConfig.get()['disable_custom_urls']) {
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
const flows = order.map(type => this.state.flows.find(flow => flow.type === type)).filter(Boolean);
return <React.Fragment>
<SignInToText
serverConfig={this.props.serverConfig}
onEditServerDetailsClick={onEditServerDetailsClick}
/>
{ flows.map(flow => {
const stepRenderer = this.stepRendererMap[flow.type];
return <React.Fragment key={flow.type}>{ stepRenderer() }</React.Fragment>
}) }
</React.Fragment>
}
private renderPasswordStep = () => {
return (
<PasswordLogin
onSubmit={this.onPasswordLogin}
onEditServerDetailsClick={onEditServerDetailsClick}
username={this.state.username}
phoneCountry={this.state.phoneCountry}
phoneNumber={this.state.phoneNumber}
@ -598,29 +588,16 @@ export default class LoginComponent extends React.Component<IProps, IState> {
};
private renderSsoStep = loginType => {
let onEditServerDetailsClick = null;
// If custom URLs are allowed, wire up the server details edit link.
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
// XXX: This link does *not* have a target="_blank" because single sign-on relies on
// redirecting the user back to a URI once they're logged in. On the web, this means
// we use the same window and redirect back to Element. On Electron, this actually
// opens the SSO page in the Electron app itself due to
// https://github.com/electron/electron/issues/8841 and so happens to work.
// If this bug gets fixed, it will break SSO since it will open the SSO page in the
// user's browser, let them log into their SSO provider, then redirect their browser
// to vector://vector which, of course, will not work.
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
return (
<div>
<SignInToText serverConfig={this.props.serverConfig}
onEditServerDetailsClick={onEditServerDetailsClick} />
<SSOButton
className="mx_Login_sso_link mx_Login_submit"
<SSOButtons
matrixClient={this.loginLogic.createTemporaryClient()}
flow={flow}
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
/>
</div>
);
@ -689,7 +666,7 @@ export default class LoginComponent extends React.Component<IProps, IState> {
{ errorTextSection }
{ serverDeadSection }
{ this.renderServerComponent() }
{ this.renderLoginComponentForStep() }
{ this.renderLoginComponentForFlows() }
{ footer }
</AuthBody>
</AuthPage>