Merge pull request #729 from matrix-org/dbkr/register_ui_auth

Port registration over to use InteractiveAuth
This commit is contained in:
David Baker 2017-03-03 13:37:41 +00:00 committed by GitHub
commit 1bdf213d67
14 changed files with 673 additions and 964 deletions

View file

@ -27,6 +27,9 @@ export default React.createClass({
displayName: 'InteractiveAuth',
propTypes: {
// matrix client to use for UI auth requests
matrixClient: React.PropTypes.object.isRequired,
// response from initial request. If not supplied, will do a request on
// mount.
authData: React.PropTypes.shape({
@ -38,11 +41,27 @@ export default React.createClass({
// callback
makeRequest: React.PropTypes.func.isRequired,
// callback called when the auth process has finished
// callback called when the auth process has finished,
// successfully or unsuccessfully.
// @param {bool} status True if the operation requiring
// auth was completed sucessfully, false if canceled.
// @param result The result of the authenticated call
onFinished: React.PropTypes.func.isRequired,
onAuthFinished: React.PropTypes.func.isRequired,
// Inputs provided by the user to the auth process
// and used by various stages. As passed to js-sdk
// interactive-auth
inputs: React.PropTypes.object,
// As js-sdk interactive-auth
makeRegistrationUrl: React.PropTypes.func,
sessionId: React.PropTypes.string,
clientSecret: React.PropTypes.string,
emailSid: React.PropTypes.string,
// If true, poll to see if the auth flow has been completed
// out-of-band
poll: React.PropTypes.bool,
},
getInitialState: function() {
@ -60,12 +79,18 @@ export default React.createClass({
this._authLogic = new InteractiveAuth({
authData: this.props.authData,
doRequest: this._requestCallback,
startAuthStage: this._startAuthStage,
inputs: this.props.inputs,
stateUpdated: this._authStateUpdated,
matrixClient: this.props.matrixClient,
sessionId: this.props.sessionId,
clientSecret: this.props.clientSecret,
emailSid: this.props.emailSid,
});
this._authLogic.attemptAuth().then((result) => {
this.props.onFinished(true, result);
this.props.onAuthFinished(true, result);
}).catch((error) => {
this.props.onAuthFinished(false, error);
console.error("Error during user-interactive auth:", error);
if (this._unmounted) {
return;
@ -76,17 +101,32 @@ export default React.createClass({
errorText: msg
});
}).done();
this._intervalId = null;
if (this.props.poll) {
this._intervalId = setInterval(() => {
this._authLogic.poll();
}, 2000);
}
},
componentWillUnmount: function() {
this._unmounted = true;
if (this._intervalId !== null) {
clearInterval(this._intervalId);
}
},
_startAuthStage: function(stageType, error) {
_authStateUpdated: function(stageType, stageState) {
const oldStage = this.state.authStage;
this.setState({
authStage: stageType,
errorText: error ? error.error : null,
}, this._setFocus);
stageState: stageState,
errorText: stageState.error,
}, () => {
if (oldStage != stageType) this._setFocus();
});
},
_requestCallback: function(auth) {
@ -117,19 +157,35 @@ export default React.createClass({
_renderCurrentStage: function() {
const stage = this.state.authStage;
var StageComponent = getEntryComponentForLoginType(stage);
if (!stage) return null;
const StageComponent = getEntryComponentForLoginType(stage);
return (
<StageComponent ref="stageComponent"
loginType={stage}
matrixClient={this.props.matrixClient}
authSessionId={this._authLogic.getSessionId()}
clientSecret={this._authLogic.getClientSecret()}
stageParams={this._authLogic.getStageParams(stage)}
submitAuthDict={this._submitAuthDict}
errorText={this.state.stageErrorText}
busy={this.state.busy}
inputs={this.props.inputs}
stageState={this.state.stageState}
fail={this._onAuthStageFailed}
setEmailSid={this._setEmailSid}
makeRegistrationUrl={this.props.makeRegistrationUrl}
/>
);
},
_onAuthStageFailed: function(e) {
this.props.onAuthFinished(false, e);
},
_setEmailSid: function(sid) {
this._authLogic.setEmailSid(sid);
},
render: function() {
let error = null;
if (this.state.errorText) {

View file

@ -1,5 +1,6 @@
/*
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.
@ -65,6 +66,9 @@ module.exports = React.createClass({
// displayname, if any, to set on the device when logging
// in/registering.
defaultDeviceDisplayName: React.PropTypes.string,
// A function that makes a registration URL
makeRegistrationUrl: React.PropTypes.func.isRequired,
},
childContextTypes: {
@ -324,23 +328,19 @@ module.exports = React.createClass({
Lifecycle.logout();
break;
case 'start_registration':
var newState = payload.params || {};
newState.screen = 'register';
if (
payload.params &&
payload.params.client_secret &&
payload.params.session_id &&
payload.params.hs_url &&
payload.params.is_url &&
payload.params.sid
) {
newState.register_client_secret = payload.params.client_secret;
newState.register_session_id = payload.params.session_id;
newState.register_hs_url = payload.params.hs_url;
newState.register_is_url = payload.params.is_url;
newState.register_id_sid = payload.params.sid;
}
this.setStateForNewScreen(newState);
const params = payload.params || {};
this.setStateForNewScreen({
screen: 'register',
// these params may be undefined, but if they are,
// unset them from our state: we don't want to
// resume a previous registration session if the
// user just clicked 'register'
register_client_secret: params.client_secret,
register_session_id: params.session_id,
register_hs_url: params.hs_url,
register_is_url: params.is_url,
register_id_sid: params.sid,
});
this.notifyNewScreen('register');
break;
case 'start_login':
@ -356,13 +356,22 @@ module.exports = React.createClass({
});
break;
case 'start_upgrade_registration':
// stash our guest creds so we can backout if needed
// also stash our credentials, then if we restore the session,
// we can just do it the same way whether we started upgrade
// registration or explicitly logged out
this.guestCreds = MatrixClientPeg.getCredentials();
this.setStateForNewScreen({
screen: "register",
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
guestAccessToken: MatrixClientPeg.get().getAccessToken(),
});
// stop the client: if we are syncing whilst the registration
// is completed in another browser, we'll be 401ed for using
// a guest access token for a non-guest account.
// It will be restarted in onReturnToGuestClick
Lifecycle.stopMatrixClient();
this.notifyNewScreen('register');
break;
case 'start_password_recovery':
@ -1069,6 +1078,13 @@ module.exports = React.createClass({
this.setState({currentRoomId: room_id});
},
_makeRegistrationUrl: function(params) {
if (this.props.startingFragmentQueryParams.referrer) {
params.referrer = this.props.startingFragmentQueryParams.referrer;
}
return this.props.makeRegistrationUrl(params);
},
render: function() {
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
var LoggedInView = sdk.getComponent('structures.LoggedInView');
@ -1132,7 +1148,7 @@ module.exports = React.createClass({
teamServerConfig={this.props.config.teamServerConfig}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl}
makeRegistrationUrl={this._makeRegistrationUrl}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick}

View file

@ -19,13 +19,13 @@ limitations under the License.
var React = require('react');
var ReactDOM = require('react-dom');
var sdk = require('../../../index');
var Signup = require("../../../Signup");
var Login = require("../../../Login");
var PasswordLogin = require("../../views/login/PasswordLogin");
var CasLogin = require("../../views/login/CasLogin");
var ServerConfig = require("../../views/login/ServerConfig");
/**
* A wire component which glues together login UI components and Signup logic
* A wire component which glues together login UI components and Login logic
*/
module.exports = React.createClass({
displayName: 'Login',
@ -146,7 +146,7 @@ module.exports = React.createClass({
var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
var loginLogic = new Signup.Login(hsUrl, isUrl, fallbackHsUrl, {
var loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
});
this._loginLogic = loginLogic;

View file

@ -1,5 +1,6 @@
/*
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.
@ -14,10 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import Matrix from 'matrix-js-sdk';
import q from 'q';
import React from 'react';
import sdk from '../../../index';
@ -31,10 +31,6 @@ import RtsClient from '../../../RtsClient';
const MIN_PASSWORD_LENGTH = 6;
/**
* TODO: It would be nice to make use of the InteractiveAuthEntryComponents
* here, rather than inventing our own.
*/
module.exports = React.createClass({
displayName: 'Registration',
@ -42,7 +38,7 @@ module.exports = React.createClass({
onLoggedIn: React.PropTypes.func.isRequired,
clientSecret: React.PropTypes.string,
sessionId: React.PropTypes.string,
registrationUrl: React.PropTypes.string,
makeRegistrationUrl: React.PropTypes.func.isRequired,
idSid: React.PropTypes.string,
customHsUrl: React.PropTypes.string,
customIsUrl: React.PropTypes.string,
@ -83,27 +79,20 @@ module.exports = React.createClass({
formVals: {
email: this.props.email,
},
// true if we're waiting for the user to complete
// user-interactive auth
// If we've been given a session ID, we're resuming
// straight back into UI auth
doingUIAuth: Boolean(this.props.sessionId),
hsUrl: this.props.customHsUrl,
isUrl: this.props.customIsUrl,
};
},
componentWillMount: function() {
this._unmounted = false;
this.dispatcherRef = dis.register(this.onAction);
// attach this to the instance rather than this.state since it isn't UI
this.registerLogic = new Signup.Register(
this.props.customHsUrl, this.props.customIsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
}
);
this.registerLogic.setClientSecret(this.props.clientSecret);
this.registerLogic.setSessionId(this.props.sessionId);
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
this.registerLogic.setIdSid(this.props.idSid);
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
if (this.props.referrer) {
this.registerLogic.setReferrer(this.props.referrer);
}
this.registerLogic.recheckState();
this._replaceClient();
if (
this.props.teamServerConfig &&
@ -135,154 +124,124 @@ module.exports = React.createClass({
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
this._unmounted = true;
},
componentDidMount: function() {
// may have already done an HTTP hit (e.g. redirect from an email) so
// check for any pending response
var promise = this.registerLogic.getPromise();
if (promise) {
this.onProcessingRegistration(promise);
}
},
onHsUrlChanged: function(newHsUrl) {
this.registerLogic.setHomeserverUrl(newHsUrl);
this.setState({
hsUrl: newHsUrl,
});
this._replaceClient();
},
onIsUrlChanged: function(newIsUrl) {
this.registerLogic.setIdentityServerUrl(newIsUrl);
this.setState({
isUrl: newIsUrl,
});
this._replaceClient();
},
onAction: function(payload) {
if (payload.action !== "registration_step_update") {
return;
}
// If the registration state has changed, this means the
// user now needs to do something. It would be better
// to expose the explicitly in the register logic.
this.setState({
busy: false
_replaceClient: function() {
this._matrixClient = Matrix.createClient({
baseUrl: this.state.hsUrl,
idBaseUrl: this.state.isUrl,
});
},
onFormSubmit: function(formVals) {
var self = this;
this.setState({
errorText: "",
busy: true,
formVals: formVals,
doingUIAuth: true,
});
if (formVals.username !== this.props.username) {
// don't try to upgrade if we changed our username
this.registerLogic.setGuestAccessToken(null);
}
this.onProcessingRegistration(this.registerLogic.register(formVals));
},
// Promise is resolved when the registration process is FULLY COMPLETE
onProcessingRegistration: function(promise) {
var self = this;
promise.done(function(response) {
self.setState({
busy: false
_onUIAuthFinished: function(success, response) {
if (!success) {
this.setState({
busy: false,
doingUIAuth: false,
errorText: response.message || response.toString(),
});
if (!response || !response.access_token) {
console.warn(
"FIXME: Register fulfilled without a final response, " +
"did you break the promise chain?"
);
// no matter, we'll grab it direct
response = self.registerLogic.getCredentials();
}
if (!response || !response.user_id || !response.access_token) {
console.error("Final response is missing keys.");
self.setState({
errorText: "Registration failed on server"
});
return;
}
return;
}
// Done regardless of `teamSelected`. People registering with non-team emails
// will just nop. The point of this being we might not have the email address
// that the user registered with at this stage (depending on whether this
// is the client they initiated registration).
let trackPromise = q(null);
if (self._rtsClient) {
// Track referral if self.props.referrer set, get team_token in order to
// retrieve team config and see welcome page etc.
trackPromise = self._rtsClient.trackReferral(
self.props.referrer || '', // Default to empty string = not referred
self.registerLogic.params.idSid,
self.registerLogic.params.clientSecret
).then((data) => {
const teamToken = data.team_token;
// Store for use /w welcome pages
window.localStorage.setItem('mx_team_token', teamToken);
self._rtsClient.getTeam(teamToken).then((team) => {
console.log(
`User successfully registered with team ${team.name}`
);
if (!team.rooms) {
return;
}
// Auto-join rooms
team.rooms.forEach((room) => {
if (room.auto_join && room.room_id) {
console.log(`Auto-joining ${room.room_id}`);
MatrixClientPeg.get().joinRoom(room.room_id);
}
});
}, (err) => {
console.error('Error getting team config', err);
});
return teamToken;
}, (err) => {
console.error('Error tracking referral', err);
});
}
return trackPromise.then((teamToken) => {
console.info('Team token promise',teamToken);
self.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: self.registerLogic.getHomeserverUrl(),
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
accessToken: response.access_token
}, teamToken);
}).then(() => {
self._setupPushers();
});
}, function(err) {
if (err.message) {
self.setState({
errorText: err.message
});
}
self.setState({
busy: false
});
console.log(err);
this.setState({
// we're still busy until we get unmounted: don't show the registration form again
busy: true,
doingUIAuth: false,
});
this.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this.state.hsUrl,
identityServerUrl: this.state.isUrl,
accessToken: response.access_token,
});
// Done regardless of `teamSelected`. People registering with non-team emails
// will just nop. The point of this being we might not have the email address
// that the user registered with at this stage (depending on whether this
// is the client they initiated registration).
let trackPromise = q(null);
if (this._rtsClient) {
// Track referral if this.props.referrer set, get team_token in order to
// retrieve team config and see welcome page etc.
trackPromise = this._rtsClient.trackReferral(
this.props.referrer || '', // Default to empty string = not referred
this.registerLogic.params.idSid,
this.registerLogic.params.clientSecret
).then((data) => {
const teamToken = data.team_token;
// Store for use /w welcome pages
window.localStorage.setItem('mx_team_token', teamToken);
this.props.onTeamMemberRegistered(teamToken);
this._rtsClient.getTeam(teamToken).then((team) => {
console.log(
`User successfully registered with team ${team.name}`
);
if (!team.rooms) {
return;
}
// Auto-join rooms
team.rooms.forEach((room) => {
if (room.auto_join && room.room_id) {
console.log(`Auto-joining ${room.room_id}`);
MatrixClientPeg.get().joinRoom(room.room_id);
}
});
}, (err) => {
console.error('Error getting team config', err);
});
return teamToken;
}, (err) => {
console.error('Error tracking referral', err);
});
}
trackPromise.then((teamToken) => {
console.info('Team token promise',teamToken);
this.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this.registerLogic.getHomeserverUrl(),
identityServerUrl: this.registerLogic.getIdentityServerUrl(),
accessToken: response.access_token
}, teamToken);
}).then(() => {
return this._setupPushers();
}).done();
},
_setupPushers: function() {
if (!this.props.brand) {
return;
return q();
}
MatrixClientPeg.get().getPushers().done((resp)=>{
var pushers = resp.pushers;
for (var i = 0; i < pushers.length; ++i) {
return MatrixClientPeg.get().getPushers().then((resp)=>{
const pushers = resp.pushers;
for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind == 'email') {
var emailPusher = pushers[i];
const emailPusher = pushers[i];
emailPusher.data = { brand: this.props.brand };
MatrixClientPeg.get().setPusher(emailPusher).done(() => {
console.log("Set email branding to " + this.props.brand);
@ -327,116 +286,114 @@ module.exports = React.createClass({
});
},
onCaptchaResponse: function(response) {
this.registerLogic.tellStage("m.login.recaptcha", {
response: response
});
},
onTeamSelected: function(teamSelected) {
if (!this._unmounted) {
this.setState({ teamSelected });
}
},
_getRegisterContentJsx: function() {
const Spinner = sdk.getComponent("elements.Spinner");
_makeRegisterRequest: function(auth) {
let guestAccessToken = this.props.guestAccessToken;
var currStep = this.registerLogic.getStep();
var registerStep;
switch (currStep) {
case "Register.COMPLETE":
break; // NOP
case "Register.START":
case "Register.STEP_m.login.dummy":
// NB. Our 'username' prop is specifically for upgrading
// a guest account
if (this.state.teamServerBusy) {
registerStep = <Spinner />;
break;
}
registerStep = (
if (
this.state.formVals.username !== this.props.username ||
this.state.hsUrl != this.props.defaultHsUrl
) {
// don't try to upgrade if we changed our username
// or are registering on a different HS
guestAccessToken = null;
}
return this._matrixClient.register(
this.state.formVals.username,
this.state.formVals.password,
undefined, // session id: included in the auth dict already
auth,
// Only send the bind_email param if we're sending username / pw params
// (Since we need to send no params at all to use the ones saved in the
// session).
Boolean(this.state.formVals.username) || undefined,
guestAccessToken,
);
},
_getUIAuthInputs() {
return {
emailAddress: this.state.formVals.email,
phoneCountry: this.state.formVals.phoneCountry,
phoneNumber: this.state.formVals.phoneNumber,
}
},
render: function() {
const LoginHeader = sdk.getComponent('login.LoginHeader');
const LoginFooter = sdk.getComponent('login.LoginFooter');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent("elements.Spinner");
const ServerConfig = sdk.getComponent('views.login.ServerConfig');
let registerBody;
if (this.state.doingUIAuth) {
registerBody = (
<InteractiveAuth
matrixClient={this._matrixClient}
makeRequest={this._makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished}
inputs={this._getUIAuthInputs()}
makeRegistrationUrl={this.props.makeRegistrationUrl}
sessionId={this.props.sessionId}
clientSecret={this.props.clientSecret}
emailSid={this.props.idSid}
poll={true}
/>
);
} else if (this.state.busy || this.state.teamServerBusy) {
registerBody = <Spinner />;
} else {
let guestUsername = this.props.username;
if (this.state.hsUrl != this.props.defaultHsUrl) {
guestUsername = null;
}
let errorSection;
if (this.state.errorText) {
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>;
}
registerBody = (
<div>
<RegistrationForm
showEmail={true}
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPassword={this.state.formVals.password}
teamsConfig={this.state.teamsConfig}
guestUsername={this.props.username}
guestUsername={guestUsername}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected}
/>
);
break;
case "Register.STEP_m.login.email.identity":
registerStep = (
<div>
Please check your email to continue registration.
</div>
);
break;
case "Register.STEP_m.login.recaptcha":
var publicKey;
var serverParams = this.registerLogic.getServerData().params;
if (serverParams && serverParams["m.login.recaptcha"]) {
publicKey = serverParams["m.login.recaptcha"].public_key;
}
registerStep = (
<CaptchaForm sitePublicKey={publicKey}
onCaptchaResponse={this.onCaptchaResponse}
{errorSection}
<ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onHsUrlChanged={this.onHsUrlChanged}
onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={1000}
/>
);
break;
default:
console.error("Unknown register state: %s", currStep);
break;
}
var busySpinner;
if (this.state.busy) {
busySpinner = (
<Spinner />
</div>
);
}
var returnToAppJsx;
let returnToAppJsx;
if (this.props.onCancelClick) {
returnToAppJsx =
returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
</a>;
}
return (
<div>
<h2>Create an account</h2>
{registerStep}
<div className="mx_Login_error">{this.state.errorText}</div>
{busySpinner}
<ServerConfig ref="serverConfig"
withToggleButton={ this.registerLogic.getStep() === "Register.START" }
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onHsUrlChanged={this.onHsUrlChanged}
onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={1000} />
<div className="mx_Login_error">
</div>
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
I already have an account
</a>
{ returnToAppJsx }
</div>
);
},
render: function() {
var LoginHeader = sdk.getComponent('login.LoginHeader');
var LoginFooter = sdk.getComponent('login.LoginFooter');
);
}
return (
<div className="mx_Login">
<div className="mx_Login_box">
@ -446,7 +403,12 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" :
null}
/>
{this._getRegisterContentJsx()}
<h2>Create an account</h2>
{registerBody}
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
I already have an account
</a>
{returnToAppJsx}
<LoginFooter />
</div>
</div>

View file

@ -27,6 +27,9 @@ export default React.createClass({
displayName: 'InteractiveAuthDialog',
propTypes: {
// matrix client to use for UI auth requests
matrixClient: React.PropTypes.object.isRequired,
// response from initial request. If not supplied, will do a request on
// mount.
authData: React.PropTypes.shape({
@ -49,22 +52,62 @@ export default React.createClass({
};
},
getInitialState: function() {
return {
authError: null,
}
},
_onAuthFinished: function(success, result) {
if (success) {
this.props.onFinished(true);
} else {
this.setState({
authError: result,
});
}
},
_onDismissClick: function() {
this.props.onFinished(false);
},
render: function() {
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let content;
if (this.state.authError) {
content = (
<div>
<div>{this.state.authError.message || this.state.authError.toString()}</div>
<br />
<AccessibleButton onClick={this._onDismissClick}
className="mx_UserSettings_button"
>
Dismiss
</AccessibleButton>
</div>
);
} else {
content = (
<div>
<InteractiveAuth ref={this._collectInteractiveAuth}
matrixClient={this.props.matrixClient}
authData={this.props.authData}
makeRequest={this.props.makeRequest}
onAuthFinished={this._onAuthFinished}
/>
</div>
);
}
return (
<BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished}
title={this.props.title}
title={this.state.authError ? 'Error' : this.props.title}
>
<div>
<InteractiveAuth ref={this._collectInteractiveAuth}
authData={this.props.authData}
makeRequest={this.props.makeRequest}
onFinished={this.props.onFinished}
/>
</div>
{content}
</BaseDialog>
);
},

View file

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@ -28,13 +27,32 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
* 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.
@ -48,6 +66,7 @@ export const PasswordAuthEntry = React.createClass({
},
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string,
// is the auth logic currently waiting for something to
@ -73,7 +92,7 @@ export const PasswordAuthEntry = React.createClass({
this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE,
user: MatrixClientPeg.get().credentials.userId,
user: this.props.matrixClient.credentials.userId,
password: this.refs.passwordField.value,
});
},
@ -164,10 +183,83 @@ export const RecaptchaAuthEntry = React.createClass({
},
});
export const EmailIdentityAuthEntry = React.createClass({
displayName: 'EmailIdentityAuthEntry',
statics: {
LOGIN_TYPE: "m.login.email.identity",
},
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
authSessionId: React.PropTypes.string.isRequired,
clientSecret: React.PropTypes.string.isRequired,
inputs: React.PropTypes.object.isRequired,
stageState: React.PropTypes.object.isRequired,
fail: React.PropTypes.func.isRequired,
setEmailSid: React.PropTypes.func.isRequired,
makeRegistrationUrl: React.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>An email has been sent to <i>{this.props.inputs.emailAddress}</i></p>
<p>Please check your email to continue registration.</p>
</div>
);
}
},
});
export const FallbackAuthEntry = React.createClass({
displayName: 'FallbackAuthEntry',
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
authSessionId: React.PropTypes.string.isRequired,
loginType: React.PropTypes.string.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
@ -189,7 +281,7 @@ export const FallbackAuthEntry = React.createClass({
},
_onShowFallbackClick: function() {
var url = MatrixClientPeg.get().getFallbackAuthUrl(
var url = this.props.matrixClient.getFallbackAuthUrl(
this.props.loginType,
this.props.authSessionId
);
@ -199,7 +291,7 @@ export const FallbackAuthEntry = React.createClass({
_onReceiveMessage: function(event) {
if (
event.data === "authDone" &&
event.origin === MatrixClientPeg.get().getHomeserverUrl()
event.origin === this.props.matrixClient.getHomeserverUrl()
) {
this.props.submitAuthDict({});
}
@ -220,6 +312,7 @@ export const FallbackAuthEntry = React.createClass({
const AuthEntryComponents = [
PasswordAuthEntry,
RecaptchaAuthEntry,
EmailIdentityAuthEntry,
];
export function getEntryComponentForLoginType(loginType) {

View file

@ -1,5 +1,6 @@
/*
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.
@ -14,18 +15,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import { field_input_incorrect } from '../../../UiEffects';
import sdk from '../../../index';
import Email from '../../../email';
import Modal from '../../../Modal';
var React = require('react');
var UiEffects = require('../../../UiEffects');
var sdk = require('../../../index');
var Email = require('../../../email');
var Modal = require("../../../Modal");
var FIELD_EMAIL = 'field_email';
var FIELD_USERNAME = 'field_username';
var FIELD_PASSWORD = 'field_password';
var FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
const FIELD_EMAIL = 'field_email';
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.
@ -54,15 +53,13 @@ module.exports = React.createClass({
// a different username will cause a fresh account to be generated.
guestUsername: React.PropTypes.string,
showEmail: React.PropTypes.bool,
minPasswordLength: React.PropTypes.number,
onError: React.PropTypes.func,
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
onRegisterClick: React.PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
},
getDefaultProps: function() {
return {
showEmail: false,
minPasswordLength: 6,
onError: function(e) {
console.error(e);
@ -174,8 +171,8 @@ module.exports = React.createClass({
showSupportEmail: false,
});
}
const valid = email === '' || Email.looksValid(email);
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
const emailValid = email === '' || Email.looksValid(email);
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
break;
case FIELD_USERNAME:
// XXX: SPEC-1
@ -227,7 +224,7 @@ module.exports = React.createClass({
fieldValid[field_id] = val;
this.setState({fieldValid: fieldValid});
if (!val) {
UiEffects.field_input_incorrect(this.fieldElementById(field_id));
field_input_incorrect(this.fieldElementById(field_id));
this.props.onError(error_code);
}
},
@ -245,8 +242,8 @@ module.exports = React.createClass({
}
},
_classForField: function(field_id, baseClass) {
let cls = baseClass || '';
_classForField: function(field_id, ...baseClasses) {
let cls = baseClasses.join(' ');
if (this.state.fieldValid[field_id] === false) {
if (cls) cls += ' ';
cls += 'error';
@ -256,44 +253,44 @@ module.exports = React.createClass({
render: function() {
var self = this;
var emailSection, belowEmailSection, registerButton;
if (this.props.showEmail) {
emailSection = (
const emailSection = (
<div>
<input type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)"
defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}}
value={self.state.email}/>
);
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">
You are registering with {this.state.selectedTeam.name}
</p>
);
}
</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">
You are registering with {this.state.selectedTeam.name}
</p>
);
}
}
if (this.props.onRegisterClick) {
registerButton = (
<input className="mx_Login_submit" type="submit" value="Register" />
);
}
var placeholderUserName = "User name";
const registerButton = (
<input className="mx_Login_submit" type="submit" value="Register" />
);
let placeholderUserName = "User name";
if (this.props.guestUsername) {
placeholderUserName += " (default: " + this.props.guestUsername + ")";
}

View file

@ -71,6 +71,7 @@ export default class DevicesPanelEntry extends React.Component {
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
Modal.createDialog(InteractiveAuthDialog, {
matrixClient: MatrixClientPeg.get(),
authData: error.data,
makeRequest: this._makeDeleteRequest,
});