Initial support for MSC2858
This commit is contained in:
parent
613710b75c
commit
1d53a5cf23
10 changed files with 237 additions and 154 deletions
|
@ -124,6 +124,7 @@
|
||||||
@import "./views/elements/_RichText.scss";
|
@import "./views/elements/_RichText.scss";
|
||||||
@import "./views/elements/_RoleButton.scss";
|
@import "./views/elements/_RoleButton.scss";
|
||||||
@import "./views/elements/_RoomAliasField.scss";
|
@import "./views/elements/_RoomAliasField.scss";
|
||||||
|
@import "./views/elements/_SSOButtons.scss";
|
||||||
@import "./views/elements/_Slider.scss";
|
@import "./views/elements/_Slider.scss";
|
||||||
@import "./views/elements/_Spinner.scss";
|
@import "./views/elements/_Spinner.scss";
|
||||||
@import "./views/elements/_StyledCheckbox.scss";
|
@import "./views/elements/_StyledCheckbox.scss";
|
||||||
|
|
|
@ -33,12 +33,6 @@ limitations under the License.
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody a.mx_Login_sso_link:link,
|
|
||||||
.mx_AuthBody a.mx_Login_sso_link:hover,
|
|
||||||
.mx_AuthBody a.mx_Login_sso_link:visited {
|
|
||||||
color: $button-primary-fg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Login_loader {
|
.mx_Login_loader {
|
||||||
display: inline;
|
display: inline;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
41
res/css/views/elements/_SSOButtons.scss
Normal file
41
res/css/views/elements/_SSOButtons.scss
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_SSOButtons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.mx_SSOButton {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
object-fit: contain;
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SSOButton_mini {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 50px; // 48px + 1px border on all sides
|
||||||
|
height: 50px; // 48px + 1px border on all sides
|
||||||
|
|
||||||
|
& + .mx_SSOButton_mini {
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
src/Login.ts
37
src/Login.ts
|
@ -30,9 +30,24 @@ interface ILoginOptions {
|
||||||
|
|
||||||
// TODO: Move this to JS SDK
|
// TODO: Move this to JS SDK
|
||||||
interface ILoginFlow {
|
interface ILoginFlow {
|
||||||
type: string;
|
type: "m.login.password" | "m.login.cas";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IIdentityProvider {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISSOFlow {
|
||||||
|
type: "m.login.sso";
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
identity_providers: IIdentityProvider[];
|
||||||
|
"org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginFlow = ISSOFlow | ILoginFlow;
|
||||||
|
|
||||||
// TODO: Move this to JS SDK
|
// TODO: Move this to JS SDK
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface ILoginParams {
|
interface ILoginParams {
|
||||||
|
@ -48,9 +63,8 @@ export default class Login {
|
||||||
private hsUrl: string;
|
private hsUrl: string;
|
||||||
private isUrl: string;
|
private isUrl: string;
|
||||||
private fallbackHsUrl: string;
|
private fallbackHsUrl: string;
|
||||||
private currentFlowIndex: number;
|
|
||||||
// TODO: Flows need a type in JS SDK
|
// TODO: Flows need a type in JS SDK
|
||||||
private flows: Array<ILoginFlow>;
|
private flows: Array<LoginFlow>;
|
||||||
private defaultDeviceDisplayName: string;
|
private defaultDeviceDisplayName: string;
|
||||||
private tempClient: MatrixClient;
|
private tempClient: MatrixClient;
|
||||||
|
|
||||||
|
@ -63,7 +77,6 @@ export default class Login {
|
||||||
this.hsUrl = hsUrl;
|
this.hsUrl = hsUrl;
|
||||||
this.isUrl = isUrl;
|
this.isUrl = isUrl;
|
||||||
this.fallbackHsUrl = fallbackHsUrl;
|
this.fallbackHsUrl = fallbackHsUrl;
|
||||||
this.currentFlowIndex = 0;
|
|
||||||
this.flows = [];
|
this.flows = [];
|
||||||
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||||
this.tempClient = null; // memoize
|
this.tempClient = null; // memoize
|
||||||
|
@ -100,27 +113,13 @@ export default class Login {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFlows(): Promise<Array<ILoginFlow>> {
|
public async getFlows(): Promise<Array<LoginFlow>> {
|
||||||
const client = this.createTemporaryClient();
|
const client = this.createTemporaryClient();
|
||||||
const { flows } = await client.loginFlows();
|
const { flows } = await client.loginFlows();
|
||||||
this.flows = flows;
|
this.flows = flows;
|
||||||
this.currentFlowIndex = 0;
|
|
||||||
// technically the UI should display options for all flows for the
|
|
||||||
// user to then choose one, so return all the flows here.
|
|
||||||
return this.flows;
|
return this.flows;
|
||||||
}
|
}
|
||||||
|
|
||||||
public chooseFlow(flowIndex): void {
|
|
||||||
this.currentFlowIndex = flowIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCurrentFlowStep(): string {
|
|
||||||
// technically the flow can have multiple steps, but no one does this
|
|
||||||
// for login so we can ignore it.
|
|
||||||
const flowStep = this.flows[this.currentFlowIndex];
|
|
||||||
return flowStep ? flowStep.type : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public loginViaPassword(
|
public loginViaPassword(
|
||||||
username: string,
|
username: string,
|
||||||
phoneCountry: string,
|
phoneCountry: string,
|
||||||
|
|
|
@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 {_t, _td} from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import Login from '../../../Login';
|
import Login, {ISSOFlow, LoginFlow} from '../../../Login';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import AuthPage from "../../views/auth/AuthPage";
|
import AuthPage from "../../views/auth/AuthPage";
|
||||||
import SSOButton from "../../views/elements/SSOButton";
|
|
||||||
import PlatformPeg from '../../../PlatformPeg';
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
|
@ -35,6 +35,7 @@ import PasswordLogin from "../../views/auth/PasswordLogin";
|
||||||
import SignInToText from "../../views/auth/SignInToText";
|
import SignInToText from "../../views/auth/SignInToText";
|
||||||
import InlineSpinner from "../../views/elements/InlineSpinner";
|
import InlineSpinner from "../../views/elements/InlineSpinner";
|
||||||
import Spinner from "../../views/elements/Spinner";
|
import Spinner from "../../views/elements/Spinner";
|
||||||
|
import SSOButtons from "../../views/elements/SSOButtons";
|
||||||
|
|
||||||
// Enable phases for login
|
// Enable phases for login
|
||||||
const PHASES_ENABLED = true;
|
const PHASES_ENABLED = true;
|
||||||
|
@ -90,17 +91,14 @@ interface IState {
|
||||||
// can we attempt to log in or are there validation errors?
|
// can we attempt to log in or are there validation errors?
|
||||||
canTryLogin: boolean;
|
canTryLogin: boolean;
|
||||||
|
|
||||||
|
phase: Phase;
|
||||||
|
flows?: LoginFlow[];
|
||||||
|
|
||||||
// used for preserving form values when changing homeserver
|
// used for preserving form values when changing homeserver
|
||||||
username: string;
|
username: string;
|
||||||
phoneCountry?: string;
|
phoneCountry?: string;
|
||||||
phoneNumber: 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 perform liveliness checks later, but for now suppress the errors.
|
||||||
// We also track the server dead errors independently of the regular errors so
|
// 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
|
// 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
|
* 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 unmounted = false;
|
||||||
private loginLogic: Login;
|
private loginLogic: Login;
|
||||||
|
|
||||||
private readonly stepRendererMap: Record<string, () => ReactNode>;
|
private readonly stepRendererMap: Record<string, () => ReactNode>;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -127,11 +126,14 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
||||||
errorText: null,
|
errorText: null,
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
canTryLogin: true,
|
canTryLogin: true,
|
||||||
|
|
||||||
|
phase: Phase.Login,
|
||||||
|
flows: null,
|
||||||
|
|
||||||
username: "",
|
username: "",
|
||||||
phoneCountry: null,
|
phoneCountry: null,
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
phase: Phase.Login,
|
|
||||||
currentFlow: null,
|
|
||||||
serverIsAlive: true,
|
serverIsAlive: true,
|
||||||
serverErrorIsFatal: false,
|
serverErrorIsFatal: false,
|
||||||
serverDeadError: "",
|
serverDeadError: "",
|
||||||
|
@ -351,13 +353,14 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
onTryRegisterClick = ev => {
|
onTryRegisterClick = ev => {
|
||||||
const step = this.getCurrentFlowStep();
|
const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password");
|
||||||
if (step === 'm.login.sso' || step === 'm.login.cas') {
|
if (!hasPasswordFlow) {
|
||||||
// If we're showing SSO it means that registration is also probably disabled,
|
// 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'.
|
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
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,
|
PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind,
|
||||||
this.props.fragmentAfterLogin);
|
this.props.fragmentAfterLogin);
|
||||||
} else {
|
} else {
|
||||||
|
@ -397,7 +400,6 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
currentFlow: null, // reset flow
|
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -432,27 +434,18 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
loginLogic.getFlows().then((flows) => {
|
loginLogic.getFlows().then((flows) => {
|
||||||
// look for a flow where we understand all of the steps.
|
// look for a flow where we understand all of the steps.
|
||||||
for (let i = 0; i < flows.length; i++ ) {
|
const supportedFlows = flows.filter(this.isSupportedFlow);
|
||||||
if (!this.isSupportedFlow(flows[i])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we just pick the first flow where we support all the
|
if (supportedFlows.length > 0) {
|
||||||
// steps. (we don't have a UI for multiple logins so let's skip
|
|
||||||
// that for now).
|
|
||||||
loginLogic.chooseFlow(i);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
currentFlow: this.getCurrentFlowStep(),
|
currentFlow: this.getCurrentFlowStep(),
|
||||||
});
|
});
|
||||||
return;
|
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({
|
this.setState({
|
||||||
errorText: _t(
|
errorText: _t("This homeserver doesn't offer any login flows which are supported by this client."),
|
||||||
"This homeserver doesn't offer any login flows which are " +
|
|
||||||
"supported by this client.",
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.setState({
|
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
|
// 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.
|
// for login and loginLogic doesn't support it so we can ignore it.
|
||||||
if (!this.stepRendererMap[flow.type]) {
|
if (!this.stepRendererMap[flow.type]) {
|
||||||
|
@ -475,13 +468,9 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
private getCurrentFlowStep() {
|
private errorTextFromError(err: MatrixError): ReactNode {
|
||||||
return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private errorTextFromError(err) {
|
|
||||||
let errCode = err.errcode;
|
let errCode = err.errcode;
|
||||||
if (!errCode && err.httpStatus) {
|
if (!errCode && err.httpStatus) {
|
||||||
errCode = "HTTP " + err.httpStatus;
|
errCode = "HTTP " + err.httpStatus;
|
||||||
|
@ -550,37 +539,38 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderLoginComponentForStep() {
|
renderLoginComponentForFlows() {
|
||||||
if (PHASES_ENABLED && this.state.phase !== Phase.Login) {
|
if (!this.state.flows) return null;
|
||||||
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;
|
let onEditServerDetailsClick = null;
|
||||||
// If custom URLs are allowed, wire up the server details edit link.
|
// 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;
|
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 (
|
return (
|
||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
onSubmit={this.onPasswordLogin}
|
onSubmit={this.onPasswordLogin}
|
||||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
|
||||||
username={this.state.username}
|
username={this.state.username}
|
||||||
phoneCountry={this.state.phoneCountry}
|
phoneCountry={this.state.phoneCountry}
|
||||||
phoneNumber={this.state.phoneNumber}
|
phoneNumber={this.state.phoneNumber}
|
||||||
|
@ -598,29 +588,16 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderSsoStep = loginType => {
|
private renderSsoStep = loginType => {
|
||||||
let onEditServerDetailsClick = null;
|
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
|
||||||
// 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.
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SignInToText serverConfig={this.props.serverConfig}
|
<SSOButtons
|
||||||
onEditServerDetailsClick={onEditServerDetailsClick} />
|
|
||||||
|
|
||||||
<SSOButton
|
|
||||||
className="mx_Login_sso_link mx_Login_submit"
|
|
||||||
matrixClient={this.loginLogic.createTemporaryClient()}
|
matrixClient={this.loginLogic.createTemporaryClient()}
|
||||||
|
flow={flow}
|
||||||
loginType={loginType}
|
loginType={loginType}
|
||||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||||
|
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -689,7 +666,7 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
||||||
{ errorTextSection }
|
{ errorTextSection }
|
||||||
{ serverDeadSection }
|
{ serverDeadSection }
|
||||||
{ this.renderServerComponent() }
|
{ this.renderServerComponent() }
|
||||||
{ this.renderLoginComponentForStep() }
|
{ this.renderLoginComponentForFlows() }
|
||||||
{ footer }
|
{ footer }
|
||||||
</AuthBody>
|
</AuthBody>
|
||||||
</AuthPage>
|
</AuthPage>
|
||||||
|
|
|
@ -24,8 +24,8 @@ import Modal from '../../../Modal';
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import {sendLoginRequest} from "../../../Login";
|
import {sendLoginRequest} from "../../../Login";
|
||||||
import AuthPage from "../../views/auth/AuthPage";
|
import AuthPage from "../../views/auth/AuthPage";
|
||||||
import SSOButton from "../../views/elements/SSOButton";
|
|
||||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||||
|
import SSOButtons from "../../views/elements/SSOButtons";
|
||||||
|
|
||||||
const LOGIN_VIEW = {
|
const LOGIN_VIEW = {
|
||||||
LOADING: 1,
|
LOADING: 1,
|
||||||
|
@ -101,10 +101,11 @@ export default class SoftLogout extends React.Component {
|
||||||
// Note: we don't use the existing Login class because it is heavily flow-based. We don't
|
// Note: we don't use the existing Login class because it is heavily flow-based. We don't
|
||||||
// care about login flows here, unless it is the single flow we support.
|
// care about login flows here, unless it is the single flow we support.
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const loginViews = (await client.loginFlows()).flows.map(f => FLOWS_TO_VIEWS[f.type]);
|
const flows = (await client.loginFlows()).flows;
|
||||||
|
const loginViews = flows.map(f => FLOWS_TO_VIEWS[f.type]);
|
||||||
|
|
||||||
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
|
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
|
||||||
this.setState({loginView: chosenView});
|
this.setState({ flows, loginView: chosenView });
|
||||||
}
|
}
|
||||||
|
|
||||||
onPasswordChange = (ev) => {
|
onPasswordChange = (ev) => {
|
||||||
|
@ -240,13 +241,18 @@ export default class SoftLogout extends React.Component {
|
||||||
introText = _t("Sign in and regain access to your account.");
|
introText = _t("Sign in and regain access to your account.");
|
||||||
} // else we already have a message and should use it (key backup warning)
|
} // 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);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{introText}</p>
|
<p>{introText}</p>
|
||||||
<SSOButton
|
<SSOButtons
|
||||||
matrixClient={MatrixClientPeg.get()}
|
matrixClient={MatrixClientPeg.get()}
|
||||||
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"}
|
flow={flow}
|
||||||
|
loginType={loginType}
|
||||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||||
|
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -26,7 +26,6 @@ import withValidation from "../elements/Validation";
|
||||||
import * as Email from "../../../email";
|
import * as Email from "../../../email";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import CountryDropdown from "./CountryDropdown";
|
import CountryDropdown from "./CountryDropdown";
|
||||||
import SignInToText from "./SignInToText";
|
|
||||||
|
|
||||||
// For validating phone numbers without country codes
|
// For validating phone numbers without country codes
|
||||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||||
|
@ -47,7 +46,6 @@ interface IProps {
|
||||||
onUsernameBlur?(username: string): void;
|
onUsernameBlur?(username: string): void;
|
||||||
onPhoneCountryChanged?(phoneCountry: string): void;
|
onPhoneCountryChanged?(phoneCountry: string): void;
|
||||||
onPhoneNumberChanged?(phoneNumber: string): void;
|
onPhoneNumberChanged?(phoneNumber: string): void;
|
||||||
onEditServerDetailsClick?(): void;
|
|
||||||
onForgotPasswordClick?(): void;
|
onForgotPasswordClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +68,6 @@ enum LoginField {
|
||||||
*/
|
*/
|
||||||
export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onEditServerDetailsClick: null,
|
|
||||||
onUsernameChanged: function() {},
|
onUsernameChanged: function() {},
|
||||||
onUsernameBlur: function() {},
|
onUsernameBlur: function() {},
|
||||||
onPhoneCountryChanged: function() {},
|
onPhoneCountryChanged: function() {},
|
||||||
|
@ -460,8 +457,6 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SignInToText serverConfig={this.props.serverConfig}
|
|
||||||
onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
|
|
||||||
<form onSubmit={this.onSubmitForm}>
|
<form onSubmit={this.onSubmitForm}>
|
||||||
{loginType}
|
{loginType}
|
||||||
{loginField}
|
{loginField}
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 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 React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import PlatformPeg from "../../../PlatformPeg";
|
|
||||||
import AccessibleButton from "./AccessibleButton";
|
|
||||||
import {_t} from "../../../languageHandler";
|
|
||||||
|
|
||||||
const SSOButton = ({matrixClient, loginType, fragmentAfterLogin, ...props}) => {
|
|
||||||
const onClick = () => {
|
|
||||||
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AccessibleButton {...props} kind="primary" onClick={onClick}>
|
|
||||||
{_t("Sign in with single sign-on")}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
SSOButton.propTypes = {
|
|
||||||
matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client
|
|
||||||
loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis
|
|
||||||
fragmentAfterLogin: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SSOButton;
|
|
111
src/components/views/elements/SSOButtons.tsx
Normal file
111
src/components/views/elements/SSOButtons.tsx
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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 React from "react";
|
||||||
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
import PlatformPeg from "../../../PlatformPeg";
|
||||||
|
import AccessibleButton from "./AccessibleButton";
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import {IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
||||||
|
idp: IIdentityProvider;
|
||||||
|
mini?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||||
|
matrixClient,
|
||||||
|
loginType,
|
||||||
|
fragmentAfterLogin,
|
||||||
|
idp,
|
||||||
|
primary,
|
||||||
|
mini,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const kind = primary ? "primary" : "primary_outline";
|
||||||
|
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon;
|
||||||
|
if (idp && idp.icon && idp.icon.startsWith("https://")) {
|
||||||
|
// TODO sanitize images
|
||||||
|
icon = <img src={idp.icon} height="24" width="24" alt={label} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = classNames("mx_SSOButton", {
|
||||||
|
mx_SSOButton_mini: mini,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mini) {
|
||||||
|
// TODO fallback icon
|
||||||
|
return (
|
||||||
|
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||||
|
{ icon }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||||
|
{ icon }
|
||||||
|
{ label }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
flow: ISSOFlow;
|
||||||
|
loginType?: "sso" | "cas";
|
||||||
|
fragmentAfterLogin?: string;
|
||||||
|
primary?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
|
||||||
|
const providers = flow.identity_providers || flow["org.matrix.msc2858.identity_providers"] || [];
|
||||||
|
if (providers.length < 2) {
|
||||||
|
return <div className="mx_SSOButtons">
|
||||||
|
<SSOButton
|
||||||
|
matrixClient={matrixClient}
|
||||||
|
loginType={loginType}
|
||||||
|
fragmentAfterLogin={fragmentAfterLogin}
|
||||||
|
idp={providers[0]}
|
||||||
|
primary={primary}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_SSOButtons">
|
||||||
|
{ providers.map(idp => (
|
||||||
|
<SSOButton
|
||||||
|
key={idp.id}
|
||||||
|
matrixClient={matrixClient}
|
||||||
|
loginType={loginType}
|
||||||
|
fragmentAfterLogin={fragmentAfterLogin}
|
||||||
|
idp={idp}
|
||||||
|
mini={true}
|
||||||
|
primary={primary}
|
||||||
|
/>
|
||||||
|
)) }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SSOButtons;
|
|
@ -1827,6 +1827,7 @@
|
||||||
"This address is available to use": "This address is available to use",
|
"This address is available to use": "This address is available to use",
|
||||||
"This address is already in use": "This address is already in use",
|
"This address is already in use": "This address is already in use",
|
||||||
"Room directory": "Room directory",
|
"Room directory": "Room directory",
|
||||||
|
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||||
"And %(count)s more...|other": "And %(count)s more...",
|
"And %(count)s more...|other": "And %(count)s more...",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue