Rename login directory to auth

This commit is contained in:
J. Ryan Stinnett 2019-01-21 16:11:10 -06:00
parent 6160db7a82
commit 29be3ee4b5
26 changed files with 45 additions and 45 deletions

View file

@ -0,0 +1,147 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
*/
'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const DIV_ID = 'mx_recaptcha';
/**
* A pure UI component which displays a captcha form.
*/
module.exports = React.createClass({
displayName: 'CaptchaForm',
propTypes: {
sitePublicKey: PropTypes.string,
// called with the captcha response
onCaptchaResponse: PropTypes.func,
},
getDefaultProps: function() {
return {
onCaptchaResponse: () => {},
};
},
getInitialState: function() {
return {
errorText: null,
};
},
componentWillMount: function() {
this._captchaWidgetId = null;
},
componentDidMount: function() {
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
// so we do this instead.
if (global.grecaptcha) {
// already loaded
this._onCaptchaLoaded();
} else {
console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
const protocol = global.location.protocol;
if (protocol === "file:") {
const warning = document.createElement('div');
// XXX: fix hardcoded app URL. Better solutions include:
// * jumping straight to a hosted captcha page (but we don't support that yet)
// * embedding the captcha in an iframe (if that works)
// * using a better captcha lib
ReactDOM.render(_t(
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
{},
{ 'a': (sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }}), warning);
this.refs.recaptchaContainer.appendChild(warning);
} else {
const scriptTag = document.createElement('script');
scriptTag.setAttribute(
'src', protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit",
);
this.refs.recaptchaContainer.appendChild(scriptTag);
}
}
},
componentWillUnmount: function() {
this._resetRecaptcha();
},
_renderRecaptcha: function(divId) {
if (!global.grecaptcha) {
console.error("grecaptcha not loaded!");
throw new Error("Recaptcha did not load successfully");
}
const publicKey = this.props.sitePublicKey;
if (!publicKey) {
console.error("No public key for recaptcha!");
throw new Error(
"This server has not supplied enough information for Recaptcha "
+ "authentication");
}
console.log("Rendering to %s", divId);
this._captchaWidgetId = global.grecaptcha.render(divId, {
sitekey: publicKey,
callback: this.props.onCaptchaResponse,
});
},
_resetRecaptcha: function() {
if (this._captchaWidgetId !== null) {
global.grecaptcha.reset(this._captchaWidgetId);
}
},
_onCaptchaLoaded: function() {
console.log("Loaded recaptcha script.");
try {
this._renderRecaptcha(DIV_ID);
} catch (e) {
this.setState({
errorText: e.toString(),
});
}
},
render: function() {
let error = null;
if (this.state.errorText) {
error = (
<div className="error">
{ this.state.errorText }
</div>
);
}
return (
<div ref="recaptchaContainer">
{ _t("This Home Server would like to make sure you are not a robot") }
<br />
<div id={DIV_ID}></div>
{ error }
</div>
);
},
});

View file

@ -0,0 +1,142 @@
/*
Copyright 2017 Vector Creations Ltd
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 sdk from '../../../index';
import { COUNTRIES } from '../../../phonenumber';
const COUNTRIES_BY_ISO2 = new Object(null);
for (const c of COUNTRIES) {
COUNTRIES_BY_ISO2[c.iso2] = c;
}
function countryMatchesSearchQuery(query, country) {
// Remove '+' if present (when searching for a prefix)
if (query[0] === '+') {
query = query.slice(1);
}
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
if (country.iso2 == query.toUpperCase()) return true;
if (country.prefix.indexOf(query) !== -1) return true;
return false;
}
export default class CountryDropdown extends React.Component {
constructor(props) {
super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this._onOptionChange = this._onOptionChange.bind(this);
this._getShortOption = this._getShortOption.bind(this);
this.state = {
searchQuery: '',
};
}
componentWillMount() {
if (!this.props.value) {
// If no value is given, we start with the first
// country selected, but our parent component
// doesn't know this, therefore we do this.
this.props.onOptionChange(COUNTRIES[0]);
}
}
_onSearchChange(search) {
this.setState({
searchQuery: search,
});
}
_onOptionChange(iso2) {
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
}
_flagImgForIso2(iso2) {
return <img src={require(`../../../../res/img/flags/${iso2}.png`)} />;
}
_getShortOption(iso2) {
if (!this.props.isSmall) {
return undefined;
}
let countryPrefix;
if (this.props.showPrefix) {
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
}
return <span>
{ this._flagImgForIso2(iso2) }
{ countryPrefix }
</span>;
}
render() {
const Dropdown = sdk.getComponent('elements.Dropdown');
let displayedCountries;
if (this.state.searchQuery) {
displayedCountries = COUNTRIES.filter(
countryMatchesSearchQuery.bind(this, this.state.searchQuery),
);
if (
this.state.searchQuery.length == 2 &&
COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]
) {
// exact ISO2 country name match: make the first result the matches ISO2
const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()];
displayedCountries = displayedCountries.filter((c) => {
return c.iso2 != matched.iso2;
});
displayedCountries.unshift(matched);
}
} else {
displayedCountries = COUNTRIES;
}
const options = displayedCountries.map((country) => {
return <div key={country.iso2}>
{ this._flagImgForIso2(country.iso2) }
{ country.name } <span>(+{ country.prefix })</span>
</div>;
});
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propgating
const value = this.props.value || COUNTRIES[0].iso2;
return <Dropdown className={this.props.className + " left_aligned"}
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._getShortOption}
value={value} searchEnabled={true} disabled={this.props.disabled}
>
{ options }
</Dropdown>;
}
}
CountryDropdown.propTypes = {
className: PropTypes.string,
isSmall: PropTypes.bool,
// if isSmall, show +44 in the selected value
showPrefix: PropTypes.bool,
onOptionChange: PropTypes.func.isRequired,
value: PropTypes.string,
disabled: PropTypes.bool,
};

View file

@ -0,0 +1,50 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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 { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'CustomServerDialog',
render: function() {
return (
<div className="mx_ErrorDialog">
<div className="mx_Dialog_title">
{ _t("Custom Server Options") }
</div>
<div className="mx_Dialog_content">
<span>
{ _t("You can use the custom server options to sign into other Matrix " +
"servers by specifying a different Home server URL.") }
<br />
{ _t("This allows you to use this app with an existing Matrix account on " +
"a different home server.") }
<br />
<br />
{ _t("You can also set a custom identity server but this will typically prevent " +
"interaction with users based on email address.") }
</span>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished} autoFocus={true}>
{ _t("Dismiss") }
</button>
</div>
</div>
);
},
});

View file

@ -0,0 +1,649 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
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 url from 'url';
import classnames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
* for an auth stage. (The intention is that they could also be used for other
* components, such as the registration flow).
*
* Call getEntryComponentForLoginType() to get a component suitable for a
* particular login type. Each component requires the same properties:
*
* matrixClient: A matrix client. May be a different one to the one
* currently being used generally (eg. to register with
* one HS whilst beign a guest on another).
* loginType: the login type of the auth stage being attempted
* authSessionId: session id from the server
* clientSecret: The client secret in use for ID server auth sessions
* stageParams: params from the server for the stage being attempted
* errorText: error message from a previous attempt to authenticate
* submitAuthDict: a function which will be called with the new auth dict
* busy: a boolean indicating whether the auth logic is doing something
* the user needs to wait for.
* inputs: Object of inputs provided by the user, as in js-sdk
* interactive-auth
* stageState: Stage-specific object used for communicating state information
* to the UI from the state-specific auth logic.
* Defined keys for stages are:
* m.login.email.identity:
* * emailSid: string representing the sid of the active
* verification session from the ID server, or
* null if no session is active.
* fail: a function which should be called with an error object if an
* error occurred during the auth stage. This will cause the auth
* session to be failed and the process to go back to the start.
* setEmailSid: m.login.email.identity only: a function to be called with the
* email sid after a token is requested.
* makeRegistrationUrl A function that makes a registration URL
*
* Each component may also provide the following functions (beyond the standard React ones):
* focus: set the input focus appropriately in the form.
*/
export const PasswordAuthEntry = React.createClass({
displayName: 'PasswordAuthEntry',
statics: {
LOGIN_TYPE: "m.login.password",
},
propTypes: {
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
// is the auth logic currently waiting for something to
// happen?
busy: PropTypes.bool,
},
getInitialState: function() {
return {
passwordValid: false,
};
},
focus: function() {
if (this.refs.passwordField) {
this.refs.passwordField.focus();
}
},
_onSubmit: function(e) {
e.preventDefault();
if (this.props.busy) return;
this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE,
user: this.props.matrixClient.credentials.userId,
password: this.refs.passwordField.value,
});
},
_onPasswordFieldChange: function(ev) {
// enable the submit button iff the password is non-empty
this.setState({
passwordValid: Boolean(this.refs.passwordField.value),
});
},
render: function() {
let passwordBoxClass = null;
if (this.props.errorText) {
passwordBoxClass = 'error';
}
let submitButtonOrSpinner;
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
submitButtonOrSpinner = <Loader />;
} else {
submitButtonOrSpinner = (
<input type="submit"
className="mx_Dialog_primary"
disabled={!this.state.passwordValid}
/>
);
}
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<p>{ _t("To continue, please enter your password.") }</p>
<form onSubmit={this._onSubmit}>
<label htmlFor="passwordField">{ _t("Password:") }</label>
<input
name="passwordField"
ref="passwordField"
className={passwordBoxClass}
onChange={this._onPasswordFieldChange}
type="password"
/>
<div className="mx_button_row">
{ submitButtonOrSpinner }
</div>
</form>
{ errorSection }
</div>
);
},
});
export const RecaptchaAuthEntry = React.createClass({
displayName: 'RecaptchaAuthEntry',
statics: {
LOGIN_TYPE: "m.login.recaptcha",
},
propTypes: {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
},
_onCaptchaResponse: function(response) {
this.props.submitAuthDict({
type: RecaptchaAuthEntry.LOGIN_TYPE,
response: response,
});
},
render: function() {
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
}
const CaptchaForm = sdk.getComponent("views.auth.CaptchaForm");
const sitePublicKey = this.props.stageParams.public_key;
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<CaptchaForm sitePublicKey={sitePublicKey}
onCaptchaResponse={this._onCaptchaResponse}
/>
{ errorSection }
</div>
);
},
});
export const TermsAuthEntry = React.createClass({
displayName: 'TermsAuthEntry',
statics: {
LOGIN_TYPE: "m.login.terms",
},
propTypes: {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
},
componentWillMount: function() {
// example stageParams:
//
// {
// "policies": {
// "privacy_policy": {
// "version": "1.0",
// "en": {
// "name": "Privacy Policy",
// "url": "https://example.org/privacy-1.0-en.html",
// },
// "fr": {
// "name": "Politique de confidentialité",
// "url": "https://example.org/privacy-1.0-fr.html",
// },
// },
// "other_policy": { ... },
// }
// }
const allPolicies = this.props.stageParams.policies || {};
const prefLang = SettingsStore.getValue("language");
const initToggles = {};
const pickedPolicies = [];
for (const policyId of Object.keys(allPolicies)) {
const policy = allPolicies[policyId];
// Pick a language based on the user's language, falling back to english,
// and finally to the first language available. If there's still no policy
// available then the homeserver isn't respecting the spec.
let langPolicy = policy[prefLang];
if (!langPolicy) langPolicy = policy["en"];
if (!langPolicy) {
// last resort
const firstLang = Object.keys(policy).find(e => e !== "version");
langPolicy = policy[firstLang];
}
if (!langPolicy) throw new Error("Failed to find a policy to show the user");
initToggles[policyId] = false;
langPolicy.id = policyId;
pickedPolicies.push(langPolicy);
}
this.setState({
"toggledPolicies": initToggles,
"policies": pickedPolicies,
});
},
tryContinue: function() {
this._trySubmit();
},
_togglePolicy: function(policyId) {
const newToggles = {};
for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id];
if (policy.id === policyId) checked = !checked;
newToggles[policy.id] = checked;
}
this.setState({"toggledPolicies": newToggles});
},
_trySubmit: function() {
let allChecked = true;
for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id];
allChecked = allChecked && checked;
}
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
else this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
},
render: function() {
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
}
const checkboxes = [];
let allChecked = true;
for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id];
allChecked = allChecked && checked;
checkboxes.push(
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onClick={() => this._togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
</label>,
);
}
let errorSection;
if (this.props.errorText || this.state.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText || this.state.errorText }
</div>
);
}
let submitButton;
if (this.props.showContinue !== false) {
// XXX: button classes
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_UserSettings_button"
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
}
return (
<div>
<p>{_t("Please review and accept the policies of this homeserver:")}</p>
{ checkboxes }
{ errorSection }
{ submitButton }
</div>
);
},
});
export const EmailIdentityAuthEntry = React.createClass({
displayName: 'EmailIdentityAuthEntry',
statics: {
LOGIN_TYPE: "m.login.email.identity",
},
propTypes: {
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
authSessionId: PropTypes.string.isRequired,
clientSecret: PropTypes.string.isRequired,
inputs: PropTypes.object.isRequired,
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
makeRegistrationUrl: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
requestingToken: false,
};
},
componentWillMount: function() {
if (this.props.stageState.emailSid === null) {
this.setState({requestingToken: true});
this._requestEmailToken().catch((e) => {
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
}).done();
}
},
/*
* Requests a verification token by email.
*/
_requestEmailToken: function() {
const nextLink = this.props.makeRegistrationUrl({
client_secret: this.props.clientSecret,
hs_url: this.props.matrixClient.getHomeserverUrl(),
is_url: this.props.matrixClient.getIdentityServerUrl(),
session_id: this.props.authSessionId,
});
return this.props.matrixClient.requestRegisterEmailToken(
this.props.inputs.emailAddress,
this.props.clientSecret,
1, // TODO: Multiple send attempts?
nextLink,
).then((result) => {
this.props.setEmailSid(result.sid);
});
},
render: function() {
if (this.state.requestingToken) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
} else {
return (
<div>
<p>{ _t("An email has been sent to %(emailAddress)s",
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
) }
</p>
<p>{ _t("Please check your email to continue registration.") }</p>
</div>
);
}
},
});
export const MsisdnAuthEntry = React.createClass({
displayName: 'MsisdnAuthEntry',
statics: {
LOGIN_TYPE: "m.login.msisdn",
},
propTypes: {
inputs: PropTypes.shape({
phoneCountry: PropTypes.string,
phoneNumber: PropTypes.string,
}),
fail: PropTypes.func,
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
submitAuthDict: PropTypes.func,
},
getInitialState: function() {
return {
token: '',
requestingToken: false,
};
},
componentWillMount: function() {
this._sid = null;
this._msisdn = null;
this._tokenBox = null;
this.setState({requestingToken: true});
this._requestMsisdnToken().catch((e) => {
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
}).done();
},
/*
* Requests a verification token by SMS.
*/
_requestMsisdnToken: function() {
return this.props.matrixClient.requestRegisterMsisdnToken(
this.props.inputs.phoneCountry,
this.props.inputs.phoneNumber,
this.props.clientSecret,
1, // TODO: Multiple send attempts?
).then((result) => {
this._sid = result.sid;
this._msisdn = result.msisdn;
});
},
_onTokenChange: function(e) {
this.setState({
token: e.target.value,
});
},
_onFormSubmit: function(e) {
e.preventDefault();
if (this.state.token == '') return;
this.setState({
errorText: null,
});
this.props.matrixClient.submitMsisdnToken(
this._sid, this.props.clientSecret, this.state.token,
).then((result) => {
if (result.success) {
const idServerParsedUrl = url.parse(
this.props.matrixClient.getIdentityServerUrl(),
);
this.props.submitAuthDict({
type: MsisdnAuthEntry.LOGIN_TYPE,
threepid_creds: {
sid: this._sid,
client_secret: this.props.clientSecret,
id_server: idServerParsedUrl.host,
},
});
} else {
this.setState({
errorText: _t("Token incorrect"),
});
}
}).catch((e) => {
this.props.fail(e);
console.log("Failed to submit msisdn token");
}).done();
},
render: function() {
if (this.state.requestingToken) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
} else {
const enableSubmit = Boolean(this.state.token);
const submitClasses = classnames({
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
mx_UserSettings_button: true, // XXX button classes
});
let errorSection;
if (this.state.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.state.errorText }
</div>
);
}
return (
<div>
<p>{ _t("A text message has been sent to %(msisdn)s",
{ msisdn: <i>{ this._msisdn }</i> },
) }
</p>
<p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}>
<input type="text"
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
value={this.state.token}
onChange={this._onTokenChange}
aria-label={ _t("Code")}
/>
<br />
<input type="submit" value={_t("Submit")}
className={submitClasses}
disabled={!enableSubmit}
/>
</form>
{errorSection}
</div>
</div>
);
}
},
});
export const FallbackAuthEntry = React.createClass({
displayName: 'FallbackAuthEntry',
propTypes: {
matrixClient: PropTypes.object.isRequired,
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
},
componentWillMount: function() {
// we have to make the user click a button, as browsers will block
// the popup if we open it immediately.
this._popupWindow = null;
window.addEventListener("message", this._onReceiveMessage);
},
componentWillUnmount: function() {
window.removeEventListener("message", this._onReceiveMessage);
if (this._popupWindow) {
this._popupWindow.close();
}
},
focus: function() {
if (this.refs.fallbackButton) {
this.refs.fallbackButton.focus();
}
},
_onShowFallbackClick: function() {
const url = this.props.matrixClient.getFallbackAuthUrl(
this.props.loginType,
this.props.authSessionId,
);
this._popupWindow = window.open(url);
},
_onReceiveMessage: function(event) {
if (
event.data === "authDone" &&
event.origin === this.props.matrixClient.getHomeserverUrl()
) {
this.props.submitAuthDict({});
}
},
render: function() {
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
{errorSection}
</div>
);
},
});
const AuthEntryComponents = [
PasswordAuthEntry,
RecaptchaAuthEntry,
EmailIdentityAuthEntry,
MsisdnAuthEntry,
TermsAuthEntry,
];
export function getEntryComponentForLoginType(loginType) {
for (const c of AuthEntryComponents) {
if (c.LOGIN_TYPE == loginType) {
return c;
}
}
return FallbackAuthEntry;
}

View file

@ -0,0 +1,32 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
*/
'use strict';
import { _t } from '../../../languageHandler';
import React from 'react';
module.exports = React.createClass({
displayName: 'LoginFooter',
render: function() {
return (
<div className="mx_Login_links">
<a href="https://matrix.org">{ _t("powered by Matrix") }</a>
</div>
);
},
});

View file

@ -0,0 +1,31 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
*/
'use strict';
const React = require('react');
module.exports = React.createClass({
displayName: 'LoginHeader',
render: function() {
return (
<div className="mx_Login_logo">
Matrix
</div>
);
},
});

View file

@ -0,0 +1,32 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
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.
*/
'use strict';
const React = require('react');
module.exports = React.createClass({
displayName: 'LoginPage',
render: function() {
return (
<div className="mx_Login">
{ this.props.children }
</div>
);
},
});

View file

@ -0,0 +1,338 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
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 classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {field_input_incorrect} from '../../../UiEffects';
import SdkConfig from '../../../SdkConfig';
/**
* A pure UI component which displays a username/password form.
*/
class PasswordLogin extends React.Component {
static defaultProps = {
onError: function() {},
onUsernameChanged: function() {},
onUsernameBlur: function() {},
onPasswordChanged: function() {},
onPhoneCountryChanged: function() {},
onPhoneNumberChanged: function() {},
initialUsername: "",
initialPhoneCountry: "",
initialPhoneNumber: "",
initialPassword: "",
loginIncorrect: false,
hsDomain: "",
hsName: null,
disableSubmit: false,
}
constructor(props) {
super(props);
this.state = {
username: this.props.initialUsername,
password: this.props.initialPassword,
phoneCountry: this.props.initialPhoneCountry,
phoneNumber: this.props.initialPhoneNumber,
loginType: PasswordLogin.LOGIN_FIELD_MXID,
};
this.onSubmitForm = this.onSubmitForm.bind(this);
this.onUsernameChanged = this.onUsernameChanged.bind(this);
this.onUsernameBlur = this.onUsernameBlur.bind(this);
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
this.onPasswordChanged = this.onPasswordChanged.bind(this);
this.isLoginEmpty = this.isLoginEmpty.bind(this);
}
componentWillMount() {
this._passwordField = null;
this._loginField = null;
}
componentWillReceiveProps(nextProps) {
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
field_input_incorrect(this.isLoginEmpty() ? this._loginField : this._passwordField);
}
}
onSubmitForm(ev) {
ev.preventDefault();
let username = ''; // XXX: Synapse breaks if you send null here:
let phoneCountry = null;
let phoneNumber = null;
let error;
switch (this.state.loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
username = this.state.username;
if (!username) {
error = _t('The email field must not be blank.');
}
break;
case PasswordLogin.LOGIN_FIELD_MXID:
username = this.state.username;
if (!username) {
error = _t('The user name field must not be blank.');
}
break;
case PasswordLogin.LOGIN_FIELD_PHONE:
phoneCountry = this.state.phoneCountry;
phoneNumber = this.state.phoneNumber;
if (!phoneNumber) {
error = _t('The phone number field must not be blank.');
}
break;
}
if (error) {
this.props.onError(error);
return;
}
if (!this.state.password) {
this.props.onError(_t('The password field must not be blank.'));
return;
}
this.props.onSubmit(
username,
phoneCountry,
phoneNumber,
this.state.password,
);
}
onUsernameChanged(ev) {
this.setState({username: ev.target.value});
this.props.onUsernameChanged(ev.target.value);
}
onUsernameBlur(ev) {
this.props.onUsernameBlur(this.state.username);
}
onLoginTypeChange(loginType) {
this.props.onError(null); // send a null error to clear any error messages
this.setState({
loginType: loginType,
username: "", // Reset because email and username use the same state
});
}
onPhoneCountryChanged(country) {
this.setState({
phoneCountry: country.iso2,
phonePrefix: country.prefix,
});
this.props.onPhoneCountryChanged(country.iso2);
}
onPhoneNumberChanged(ev) {
this.setState({phoneNumber: ev.target.value});
this.props.onPhoneNumberChanged(ev.target.value);
}
onPasswordChanged(ev) {
this.setState({password: ev.target.value});
this.props.onPasswordChanged(ev.target.value);
}
renderLoginField(loginType, disabled) {
const classes = {
mx_Login_field: true,
mx_Login_field_disabled: disabled,
};
switch (loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
classes.mx_Login_email = true;
classes.error = this.props.loginIncorrect && !this.state.username;
return <input
className={classNames(classes)}
ref={(e) => {this._loginField = e;}}
key="email_input"
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
placeholder="joe@example.com"
value={this.state.username}
autoFocus
disabled={disabled}
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
classes.mx_Login_username = true;
classes.error = this.props.loginIncorrect && !this.state.username;
return <input
className={classNames(classes)}
ref={(e) => {this._loginField = e;}}
key="username_input"
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
placeholder={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("User name")}
value={this.state.username}
autoFocus
disabled={disabled}
/>;
case PasswordLogin.LOGIN_FIELD_PHONE: {
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
classes.mx_Login_phoneNumberField = true;
classes.mx_Login_field_has_prefix = true;
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
return <div className="mx_Login_phoneSection">
<CountryDropdown
className="mx_Login_phoneCountry mx_Login_field_prefix"
onOptionChange={this.onPhoneCountryChanged}
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
disabled={disabled}
/>
<input
className={classNames(classes)}
ref={(e) => {this._loginField = e;}}
key="phone_input"
type="text"
name="phoneNumber"
onChange={this.onPhoneNumberChanged}
placeholder={_t("Mobile phone number")}
value={this.state.phoneNumber}
autoFocus
disabled={disabled}
/>
</div>;
}
}
}
isLoginEmpty() {
switch (this.state.loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
case PasswordLogin.LOGIN_FIELD_MXID:
return !this.state.username;
case PasswordLogin.LOGIN_FIELD_PHONE:
return !this.state.phoneCountry || !this.state.phoneNumber;
}
}
render() {
let forgotPasswordJsx;
if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = (
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
{ _t('Forgot your password?') }
</a>
);
}
let matrixIdText = _t('Matrix ID');
if (this.props.hsName) {
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName});
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname});
} catch (e) {
// ignore
}
}
const pwFieldClass = classNames({
mx_Login_field: true,
mx_Login_field_disabled: matrixIdText === '',
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
});
const Dropdown = sdk.getComponent('elements.Dropdown');
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
let loginType;
if (!SdkConfig.get().disable_3pid_login) {
loginType = (
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Dropdown
className="mx_Login_type_dropdown"
value={this.state.loginType}
disabled={matrixIdText === ''}
onOptionChange={this.onLoginTypeChange}>
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ matrixIdText }</span>
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span>
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
</Dropdown>
</div>
);
}
const disableSubmit = this.props.disableSubmit || matrixIdText === '';
return (
<div>
<form onSubmit={this.onSubmitForm}>
{ loginType }
{ loginField }
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password"
value={this.state.password} onChange={this.onPasswordChanged}
placeholder={_t('Password')}
disabled={matrixIdText === ''}
/>
<br />
{ forgotPasswordJsx }
<input className="mx_Login_submit" type="submit" value={_t('Sign in')} disabled={disableSubmit} />
</form>
</div>
);
}
}
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
hsName: PropTypes.string,
disableSubmit: PropTypes.bool,
};
module.exports = PasswordLogin;

View file

@ -0,0 +1,387 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
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 { field_input_incorrect } from '../../../UiEffects';
import sdk from '../../../index';
import Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_COUNTRY = 'field_phone_country';
const FIELD_PHONE_NUMBER = 'field_phone_number';
const FIELD_USERNAME = 'field_username';
const FIELD_PASSWORD = 'field_password';
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
/**
* A pure UI component which displays a registration form.
*/
module.exports = React.createClass({
displayName: 'RegistrationForm',
propTypes: {
// Values pre-filled in the input boxes when the component loads
defaultEmail: PropTypes.string,
defaultPhoneCountry: PropTypes.string,
defaultPhoneNumber: PropTypes.string,
defaultUsername: PropTypes.string,
defaultPassword: PropTypes.string,
teamsConfig: PropTypes.shape({
// Email address to request new teams
supportEmail: PropTypes.string,
teams: PropTypes.arrayOf(PropTypes.shape({
// The displayed name of the team
"name": PropTypes.string,
// The domain of team email addresses
"domain": PropTypes.string,
})).required,
}),
minPasswordLength: PropTypes.number,
onError: PropTypes.func,
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
},
getDefaultProps: function() {
return {
minPasswordLength: 6,
onError: function(e) {
console.error(e);
},
};
},
getInitialState: function() {
return {
fieldValid: {},
selectedTeam: null,
// The ISO2 country code selected in the phone number entry
phoneCountry: this.props.defaultPhoneCountry,
};
},
onSubmit: function(ev) {
ev.preventDefault();
// validate everything, in reverse order so
// the error that ends up being displayed
// is the one from the first invalid field.
// It's not super ideal that this just calls
// onError once for each invalid field.
this.validateField(FIELD_PASSWORD_CONFIRM);
this.validateField(FIELD_PASSWORD);
this.validateField(FIELD_USERNAME);
this.validateField(FIELD_PHONE_NUMBER);
this.validateField(FIELD_EMAIL);
const self = this;
if (this.allFieldsValid()) {
if (this.refs.email.value == '') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("If you don't specify an email address, you won't be able to reset your password. " +
"Are you sure?") }
</div>,
button: _t("Continue"),
onFinished: function(confirmed) {
if (confirmed) {
self._doSubmit(ev);
}
},
});
} else {
self._doSubmit(ev);
}
}
},
_doSubmit: function(ev) {
const email = this.refs.email.value.trim();
const promise = this.props.onRegisterClick({
username: this.refs.username.value.trim(),
password: this.refs.password.value.trim(),
email: email,
phoneCountry: this.state.phoneCountry,
phoneNumber: this.refs.phoneNumber ? this.refs.phoneNumber.value.trim() : '',
});
if (promise) {
ev.target.disabled = true;
promise.finally(function() {
ev.target.disabled = false;
});
}
},
/**
* Returns true if all fields were valid last time
* they were validated.
*/
allFieldsValid: function() {
const keys = Object.keys(this.state.fieldValid);
for (let i = 0; i < keys.length; ++i) {
if (this.state.fieldValid[keys[i]] == false) {
return false;
}
}
return true;
},
_isUniEmail: function(email) {
return email.endsWith('.ac.uk') || email.endsWith('.edu') || email.endsWith('matrix.org');
},
validateField: function(field_id) {
const pwd1 = this.refs.password.value.trim();
const pwd2 = this.refs.passwordConfirm.value.trim();
switch (field_id) {
case FIELD_EMAIL:
const email = this.refs.email.value;
if (this.props.teamsConfig && this._isUniEmail(email)) {
const matchingTeam = this.props.teamsConfig.teams.find(
(team) => {
return email.split('@').pop() === team.domain;
},
) || null;
this.setState({
selectedTeam: matchingTeam,
showSupportEmail: !matchingTeam,
});
this.props.onTeamSelected(matchingTeam);
} else {
this.props.onTeamSelected(null);
this.setState({
selectedTeam: null,
showSupportEmail: false,
});
}
const emailValid = email === '' || Email.looksValid(email);
if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) {
this.markFieldValid(field_id, false, "RegistrationForm.ERR_MISSING_EMAIL");
} else this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
break;
case FIELD_PHONE_NUMBER:
const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
if (this._authStepIsRequired('m.login.msisdn') && (!phoneNumberValid || phoneNumber === '')) {
this.markFieldValid(field_id, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER");
} else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break;
case FIELD_USERNAME:
const username = this.refs.username.value.trim();
if (!SAFE_LOCALPART_REGEX.test(username)) {
this.markFieldValid(
field_id,
false,
"RegistrationForm.ERR_USERNAME_INVALID",
);
} else if (username == '') {
this.markFieldValid(
field_id,
false,
"RegistrationForm.ERR_USERNAME_BLANK",
);
} else {
this.markFieldValid(field_id, true);
}
break;
case FIELD_PASSWORD:
if (pwd1 == '') {
this.markFieldValid(
field_id,
false,
"RegistrationForm.ERR_PASSWORD_MISSING",
);
} else if (pwd1.length < this.props.minPasswordLength) {
this.markFieldValid(
field_id,
false,
"RegistrationForm.ERR_PASSWORD_LENGTH",
);
} else {
this.markFieldValid(field_id, true);
}
break;
case FIELD_PASSWORD_CONFIRM:
this.markFieldValid(
field_id, pwd1 == pwd2,
"RegistrationForm.ERR_PASSWORD_MISMATCH",
);
break;
}
},
markFieldValid: function(field_id, val, error_code) {
const fieldValid = this.state.fieldValid;
fieldValid[field_id] = val;
this.setState({fieldValid: fieldValid});
if (!val) {
field_input_incorrect(this.fieldElementById(field_id));
this.props.onError(error_code);
}
},
fieldElementById(field_id) {
switch (field_id) {
case FIELD_EMAIL:
return this.refs.email;
case FIELD_PHONE_NUMBER:
return this.refs.phoneNumber;
case FIELD_USERNAME:
return this.refs.username;
case FIELD_PASSWORD:
return this.refs.password;
case FIELD_PASSWORD_CONFIRM:
return this.refs.passwordConfirm;
}
},
_classForField: function(field_id, ...baseClasses) {
let cls = baseClasses.join(' ');
if (this.state.fieldValid[field_id] === false) {
if (cls) cls += ' ';
cls += 'error';
}
return cls;
},
_onPhoneCountryChange(newVal) {
this.setState({
phoneCountry: newVal.iso2,
phonePrefix: newVal.prefix,
});
},
_authStepIsRequired(step) {
// A step is required if no flow exists which does not include that step
// (Notwithstanding setups like either email or msisdn being required)
return !this.props.flows.some((flow) => {
return !flow.stages.includes(step);
});
},
render: function() {
const self = this;
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ? _t("Email address") : _t("Email address (optional)");
const emailSection = (
<div>
<input type="text" ref="email"
autoFocus={true} placeholder={emailPlaceholder}
defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}}
value={self.state.email} />
</div>
);
let belowEmailSection;
if (this.props.teamsConfig) {
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
belowEmailSection = (
<p className="mx_Login_support">
Sorry, but your university is not registered with us just yet.&nbsp;
Email us on&nbsp;
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
{ this.props.teamsConfig.supportEmail }
</a>&nbsp;
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
</p>
);
} else if (this.state.selectedTeam) {
belowEmailSection = (
<p className="mx_Login_support">
{ _t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name}) }
</p>
);
}
}
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
let phoneSection;
if (!SdkConfig.get().disable_3pid_login) {
const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ? _t("Mobile phone number") : _t("Mobile phone number (optional)");
phoneSection = (
<div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry mx_Login_field_prefix"
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
/>
<input type="text" ref="phoneNumber"
placeholder={phonePlaceholder}
defaultValue={this.props.defaultPhoneNumber}
className={this._classForField(
FIELD_PHONE_NUMBER,
'mx_Login_phoneNumberField',
'mx_Login_field',
'mx_Login_field_has_prefix',
)}
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
value={self.state.phoneNumber}
/>
</div>
);
}
const registerButton = (
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
);
const placeholderUserName = _t("User name");
return (
<div>
<form onSubmit={this.onSubmit}>
{ emailSection }
{ belowEmailSection }
{ phoneSection }
<input type="text" ref="username"
placeholder={placeholderUserName} defaultValue={this.props.defaultUsername}
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
<br />
<input type="password" ref="password"
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD);}}
placeholder={_t("Password")} defaultValue={this.props.defaultPassword} />
<br />
<input type="password" ref="passwordConfirm"
placeholder={_t("Confirm password")}
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
defaultValue={this.props.defaultPassword} />
<br />
{ registerButton }
</form>
</div>
);
},
});

View file

@ -0,0 +1,199 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
*/
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const Modal = require('../../../Modal');
const sdk = require('../../../index');
import { _t } from '../../../languageHandler';
/**
* A pure UI component which displays the HS and IS to use.
*/
module.exports = React.createClass({
displayName: 'ServerConfig',
propTypes: {
onServerConfigChange: PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
withToggleButton: PropTypes.bool,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
},
getDefaultProps: function() {
return {
onServerConfigChange: function() {},
customHsUrl: "",
customIsUrl: "",
withToggleButton: false,
delayTimeMs: 0,
};
},
getInitialState: function() {
return {
hs_url: this.props.customHsUrl,
is_url: this.props.customIsUrl,
// if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible
configVisible: !this.props.withToggleButton ||
(this.props.customHsUrl !== this.props.defaultHsUrl) ||
(this.props.customIsUrl !== this.props.defaultIsUrl),
};
},
componentWillReceiveProps: function(newProps) {
if (newProps.customHsUrl === this.state.hs_url &&
newProps.customIsUrl === this.state.is_url) return;
this.setState({
hs_url: newProps.customHsUrl,
is_url: newProps.customIsUrl,
configVisible: !newProps.withToggleButton ||
(newProps.customHsUrl !== newProps.defaultHsUrl) ||
(newProps.customIsUrl !== newProps.defaultIsUrl),
});
this.props.onServerConfigChange({
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
});
},
onHomeserverChanged: function(ev) {
this.setState({hs_url: ev.target.value}, function() {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
let hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
this.props.onServerConfigChange({
hsUrl: this.state.hs_url,
isUrl: this.state.is_url,
});
});
});
},
onIdentityServerChanged: function(ev) {
this.setState({is_url: ev.target.value}, function() {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
let isUrl = this.state.is_url.trim().replace(/\/$/, "");
if (isUrl === "") isUrl = this.props.defaultIsUrl;
this.props.onServerConfigChange({
hsUrl: this.state.hs_url,
isUrl: this.state.is_url,
});
});
});
},
_waitThenInvoke: function(existingTimeoutId, fn) {
if (existingTimeoutId) {
clearTimeout(existingTimeoutId);
}
return setTimeout(fn.bind(this), this.props.delayTimeMs);
},
onServerConfigVisibleChange: function(visible, ev) {
this.setState({
configVisible: visible,
});
if (!visible) {
this.props.onServerConfigChange({
hsUrl: this.props.defaultHsUrl,
isUrl: this.props.defaultIsUrl,
});
} else {
this.props.onServerConfigChange({
hsUrl: this.state.hs_url,
isUrl: this.state.is_url,
});
}
},
showHelpPopup: function() {
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
},
render: function() {
const serverConfigStyle = {};
serverConfigStyle.display = this.state.configVisible ? 'block' : 'none';
let toggleButton;
if (this.props.withToggleButton) {
toggleButton = (
<div className="mx_ServerConfig_selector">
<input className="mx_Login_radio" id="basic" name="configVisible" type="radio"
checked={!this.state.configVisible}
onChange={this.onServerConfigVisibleChange.bind(this, false)} />
<label className="mx_Login_label" htmlFor="basic">
{ _t("Default server") }
</label>
&nbsp;&nbsp;
<input className="mx_Login_radio" id="advanced" name="configVisible" type="radio"
checked={this.state.configVisible}
onChange={this.onServerConfigVisibleChange.bind(this, true)} />
<label className="mx_Login_label" htmlFor="advanced">
{ _t("Custom server") }
</label>
</div>
);
}
return (
<div>
{ toggleButton }
<div style={serverConfigStyle}>
<div className="mx_ServerConfig">
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
{ _t("Home server URL") }
</label>
<input className="mx_Login_field" id="hsurl" type="text"
placeholder={this.props.defaultHsUrl}
disabled={!this.props.withToggleButton}
value={this.state.hs_url}
onChange={this.onHomeserverChanged} />
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
{ _t("Identity server URL") }
</label>
<input className="mx_Login_field" id="isurl" type="text"
placeholder={this.props.defaultIsUrl}
disabled={!this.props.withToggleButton}
value={this.state.is_url}
onChange={this.onIdentityServerChanged} />
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
{ _t("What does this mean?") }
</a>
</div>
</div>
</div>
);
},
});