Merge pull request #4 from matrix-org/registration

Registration
This commit is contained in:
David Baker 2015-07-16 09:58:53 +01:00
commit e2757b3587
11 changed files with 553 additions and 12 deletions

View file

@ -24,7 +24,52 @@ var React = require("react");
// maps cannot pass through two stages).
var MatrixReactSdk = require("../../src/index");
React.render(
<MatrixReactSdk.MatrixChat />,
// Here, we do some crude URL analysis to allow
// deep-linking. We only support registration
// deep-links in this example.
function routeUrl(location) {
if (location.hash.indexOf('#/register') == 0) {
var hashparts = location.hash.split('?');
var params = {};
if (hashparts.length == 2) {
var pairs = hashparts[1].split('&');
for (var i = 0; i < pairs.length; ++i) {
var parts = pairs[i].split('=');
if (parts.length != 2) continue;
params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
}
}
window.matrixChat.showScreen('register', params);
}
}
var loaded = false;
window.onload = function() {
routeUrl(window.location);
loaded = true;
}
// This will be called whenever the SDK changes screens,
// so a web page can update the URL bar appropriately.
var onNewScreen = function(screen) {
if (!loaded) return;
window.location.hash = '#/'+screen;
}
// We use this to work out what URL the SDK should
// pass through when registering to allow the user to
// click back to the client having registered.
// It's up to us to recognise if we're loaded with
// this URL and tell MatrixClient to resume registration.
var makeRegistrationUrl = function() {
return window.location.protocol + '//' +
window.location.host +
window.location.pathname +
'#/register';
}
window.matrixChat = React.render(
<MatrixReactSdk.MatrixChat onNewScreen={onNewScreen} registrationUrl={makeRegistrationUrl()} />,
document.getElementById('matrixchat')
);

View file

@ -38,6 +38,12 @@ module.exports = React.createClass({
mixins: [RoomViewController],
render: function() {
if (!this.state.room) {
return (
<div />
);
}
var myUserId = MatrixClientPeg.get().credentials.userId;
if (this.state.room.currentState.members[myUserId].membership == 'invite') {
if (this.state.joining) {

View file

@ -23,6 +23,7 @@ var RoomList = ComponentBroker.get('organisms/RoomList');
var RoomView = ComponentBroker.get('organisms/RoomView');
var MatrixToolbar = ComponentBroker.get('molecules/MatrixToolbar');
var Login = ComponentBroker.get('templates/Login');
var Register = ComponentBroker.get('templates/Register');
var MatrixChatController = require("../../../../src/controllers/pages/MatrixChat");
@ -51,6 +52,14 @@ module.exports = React.createClass({
return (
<Loader />
);
} else if (this.state.screen == 'register') {
return (
<Register onLoggedIn={this.onLoggedIn} clientSecret={this.state.register_client_secret}
sessionId={this.state.register_session_id} idSid={this.state.register_id_sid}
hsUrl={this.state.register_hs_url} isUrl={this.state.register_is_url}
registrationUrl={this.props.registrationUrl}
/>
);
} else {
return (
<Login onLoggedIn={this.onLoggedIn} />

View file

@ -40,6 +40,7 @@ module.exports = React.createClass({
<h1>Please log in:</h1>
{this.componentForStep(this.state.step)}
<div className="error">{this.state.errorText}</div>
<a onClick={this.showRegister} href="#">Create a new account</a>
</div>
);
}

View file

@ -0,0 +1,55 @@
/*
Copyright 2015 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';
var React = require('react');
var ComponentBroker = require("../../../../src/ComponentBroker");
var Loader = require("react-loader");
var RegisterController = require("../../../../src/controllers/templates/Register");
module.exports = React.createClass({
displayName: 'Register',
mixins: [RegisterController],
registerContent: function() {
if (this.state.busy) {
return (
<Loader />
);
} else {
return (
<div>
<h1>Create a new account:</h1>
{this.componentForStep(this.state.step)}
<div className="error">{this.state.errorText}</div>
<a onClick={this.showLogin} href="#">Sign in with existing account</a>
</div>
);
}
},
render: function() {
return (
<div className="mx_Register">
{this.registerContent()}
</div>
);
}
});

View file

@ -85,6 +85,7 @@ require('../skins/base/views/molecules/MemberTile');
require('../skins/base/views/organisms/RoomList');
require('../skins/base/views/organisms/RoomView');
require('../skins/base/views/templates/Login');
require('../skins/base/views/templates/Register');
require('../skins/base/views/organisms/Notifier');
require('../skins/base/views/organisms/CreateRoom');
require('../skins/base/views/molecules/UserSelector');

View file

@ -24,11 +24,13 @@ var matrixClient = null;
var localStorage = window.localStorage;
if (localStorage) {
var hs_url = localStorage.getItem("mx_hs_url");
var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
var access_token = localStorage.getItem("mx_access_token");
var user_id = localStorage.getItem("mx_user_id");
if (access_token && user_id && hs_url) {
matrixClient = Matrix.createClient({
baseUrl: hs_url,
idBaseUrl: is_url,
accessToken: access_token,
userId: user_id
});
@ -44,8 +46,11 @@ module.exports = {
matrixClient = cli;
},
replaceUsingUrl: function(hs_url) {
matrixClient = Matrix.createClient(hs_url);
replaceUsingUrls: function(hs_url, is_url) {
matrixClient = Matrix.createClient({
baseUrl: hs_url,
idBaseUrl: is_url
});
}
};

View file

@ -36,7 +36,7 @@ var tileTypes = {
module.exports = {
getInitialState: function() {
return {
room: MatrixClientPeg.get().getRoom(this.props.roomId),
room: this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null,
messageCap: INITIAL_SIZE
}
},

View file

@ -44,6 +44,11 @@ module.exports = {
this.focusComposer = false;
document.addEventListener("keydown", this.onKeyDown);
window.addEventListener("focus", this.onFocus);
if (this.state.logged_in) {
this.notifyNewScreen('');
} else {
this.notifyNewScreen('login');
}
},
componentWillUnmount: function() {
@ -63,14 +68,45 @@ module.exports = {
switch (payload.action) {
case 'logout':
this.setState({
this.replaceState({
logged_in: false,
ready: false
});
localStorage.removeItem("mx_hs_url");
localStorage.removeItem("mx_user_id");
localStorage.removeItem("mx_access_token");
Notifier.stop();
MatrixClientPeg.get().removeAllListeners();
MatrixClientPeg.replace(null);
break;
case 'start_registration':
if (this.state.logged_in) return;
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.replaceState(newState);
this.notifyNewScreen('register');
break;
case 'start_login':
if (this.state.logged_in) return;
this.replaceState({
screen: 'login'
});
this.notifyNewScreen('login');
break;
case 'view_room':
this.focusComposer = true;
this.setState({
@ -99,8 +135,12 @@ module.exports = {
},
onLoggedIn: function() {
this.setState({logged_in: true});
this.setState({
screen: undefined,
logged_in: true
});
this.startMatrixClient();
this.notifyNewScreen('');
},
startMatrixClient: function() {
@ -137,6 +177,26 @@ module.exports = {
onFocus: function(ev) {
dis.dispatch({action: 'focus_composer'});
},
showScreen(screen, params) {
if (screen == 'register') {
dis.dispatch({
action: 'start_registration',
params: params
});
} else if (screen == 'login') {
dis.dispatch({
action: 'start_login',
params: params
});
}
},
notifyNewScreen: function(screen) {
if (this.props.onNewScreen) {
this.props.onNewScreen(screen);
}
}
};

View file

@ -20,6 +20,7 @@ var React = require('react');
var MatrixClientPeg = require("../../MatrixClientPeg");
var Matrix = require("matrix-js-sdk");
var dis = require("../../dispatcher");
var ComponentBroker = require("../../ComponentBroker");
@ -41,8 +42,14 @@ module.exports = {
onHSChosen: function(ev) {
ev.preventDefault();
MatrixClientPeg.replaceUsingUrl(this.refs.serverConfig.getHsUrl());
this.setState({hs_url: this.refs.serverConfig.getHsUrl()});
MatrixClientPeg.replaceUsingUrls(
this.refs.serverConfig.getHsUrl(),
this.refs.serverConfig.getIsUrl()
);
this.setState({
hs_url: this.refs.serverConfig.getHsUrl(),
is_url: this.refs.serverConfig.getIsUrl()
});
this.setStep("fetch_stages");
var cli = MatrixClientPeg.get();
this.setState({busy: true});
@ -71,12 +78,14 @@ module.exports = {
// XXX: we assume this means we're logged in, but there could be a next stage
MatrixClientPeg.replace(Matrix.createClient({
baseUrl: that.state.hs_url,
idBaseUrl: that.state.is_url,
userId: data.user_id,
accessToken: data.access_token
}));
var localStorage = window.localStorage;
if (localStorage) {
localStorage.setItem("mx_hs_url", that.state.hs_url);
localStorage.setItem("mx_is_url", that.state.is_url);
localStorage.setItem("mx_user_id", data.user_id);
localStorage.setItem("mx_access_token", data.access_token);
} else {
@ -85,9 +94,6 @@ module.exports = {
if (that.props.onLoggedIn) {
that.props.onLoggedIn();
}
/*dis.dispatch({
'action': 'logged_in'
});*/
}, function(error) {
that.setStep("stage_m.login.password");
that.setState({errorText: 'Login failed.'});
@ -118,4 +124,11 @@ module.exports = {
);
}
},
showRegister: function(ev) {
ev.preventDefault();
dis.dispatch({
action: 'start_registration'
});
}
};

View file

@ -0,0 +1,346 @@
/*
Copyright 2015 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';
var React = require('react');
var MatrixClientPeg = require("../../MatrixClientPeg");
var Matrix = require("matrix-js-sdk");
var dis = require("../../dispatcher");
var ComponentBroker = require("../../ComponentBroker");
var ServerConfig = ComponentBroker.get("molecules/ServerConfig");
module.exports = {
getInitialState: function() {
return {
step: 'initial',
busy: false,
currentStep: 0,
totalSteps: 1
};
},
componentWillMount: function() {
this.readNewProps();
},
componentWillReceiveProps: function() {
this.readNewProps();
},
readNewProps: function() {
if (this.props.clientSecret && this.props.hsUrl &&
this.props.isUrl && this.props.sessionId &&
this.props.idSid) {
this.authSessionId = this.props.sessionId;
MatrixClientPeg.replaceUsingUrls(
this.props.hsUrl,
this.props.isUrl
);
this.setState({
hs_url: this.props.hsUrl,
is_url: this.props.isUrl
});
this.savedParams = {client_secret: this.props.clientSecret};
this.setState({busy: true});
var isLocation = document.createElement('a');
isLocation.href = this.props.isUrl;
var auth = {
type: 'm.login.email.identity',
threepid_creds: {
sid: this.props.idSid,
client_secret: this.savedParams.client_secret,
id_server: isLocation.host
}
};
this.tryRegister(auth);
}
},
componentDidUpdate: function() {
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
// so we do this instead.
if (this.refs.recaptchaContainer) {
var scriptTag = document.createElement('script');
window.mx_on_recaptcha_loaded = this.onCaptchaLoaded;
scriptTag.setAttribute('src', "https://www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit");
this.refs.recaptchaContainer.getDOMNode().appendChild(scriptTag);
}
},
setStep: function(step) {
this.setState({ step: step, errorText: '', busy: false });
},
getSupportedStageTypes: function() {
return ['m.login.email.identity', 'm.login.recaptcha'];
},
chooseFlow: function(flows) {
// this is fairly simple right now
var supportedTypes = this.getSupportedStageTypes();
var emailFlow = null;
var otherFlow = null;
for (var flowI = 0; flowI < flows.length; ++flowI) {
var flow = flows[flowI];
var flowHasEmail = false;
var flowSupported = true;
for (var stageI = 0; stageI < flow.stages.length; ++stageI) {
var stage = flow.stages[stageI];
if (supportedTypes.indexOf(stage) == -1) {
flowSupported = false;
}
if (stage == 'm.login.email.identity') {
flowHasEmail = true;
}
}
if (flowSupported) {
if (flowHasEmail) {
emailFlow = flow;
} else {
otherFlow = flow;
}
}
}
if (
this.savedParams.email != '' ||
this.completedStages.indexOf('m.login.email.identity' > -1)
) {
return emailFlow;
} else {
return otherFlow;
}
},
firstUncompletedStageIndex: function(flow) {
if (this.completedStages === undefined) return 0;
for (var i = 0; i < flow.stages.length; ++i) {
if (this.completedStages.indexOf(flow.stages[i]) == -1) {
return i;
}
}
},
numCompletedStages: function(flow) {
if (this.completedStages === undefined) return 0;
var nCompleted = 0;
for (var i = 0; i < flow.stages.length; ++i) {
if (this.completedStages.indexOf(flow.stages[i]) > -1) {
++nCompleted;
}
}
return nCompleted;
},
onInitialStageSubmit: function(ev) {
ev.preventDefault();
MatrixClientPeg.replaceUsingUrls(
this.refs.serverConfig.getHsUrl(),
this.refs.serverConfig.getIsUrl()
);
this.setState({
hs_url: this.refs.serverConfig.getHsUrl(),
is_url: this.refs.serverConfig.getIsUrl()
});
var cli = MatrixClientPeg.get();
this.setState({busy: true});
var self = this;
this.savedParams = {
email: this.refs.email.getDOMNode().value,
username: this.refs.username.getDOMNode().value,
password: this.refs.password.getDOMNode().value
};
this.tryRegister();
},
startStage: function(stageName) {
var self = this;
this.setStep('stage_'+stageName);
switch(stageName) {
case 'm.login.email.identity':
self.setState({
busy: true
});
var cli = MatrixClientPeg.get();
this.savedParams.client_secret = cli.generateClientSecret();
this.savedParams.send_attempt = 1;
var nextLink = this.props.registrationUrl +
'?client_secret=' +
encodeURIComponent(this.savedParams.client_secret) +
"&hs_url=" +
encodeURIComponent(this.state.hs_url) +
"&is_url=" +
encodeURIComponent(this.state.is_url) +
"&session_id=" +
encodeURIComponent(this.authSessionId);
cli.requestEmailToken(
this.savedParams.email,
this.savedParams.client_secret,
this.savedParams.send_attempt,
nextLink
).done(function(response) {
self.setState({
busy: false,
});
self.setStep('stage_m.login.email.identity');
}, function(error) {
self.setState({
busy: false,
errorText: 'Unable to contact the given Home Server'
});
});
break;
case 'm.login.recaptcha':
if (!this.authParams || !this.authParams['m.login.recaptcha'].public_key) {
this.setState({
errorText: "This server has not supplied enough information for Recaptcha authentication"
});
}
break;
}
},
onRegistered: function(user_id, access_token) {
MatrixClientPeg.replace(Matrix.createClient({
baseUrl: this.state.hs_url,
idBaseUrl: this.state.is_url,
userId: user_id,
accessToken: access_token
}));
var localStorage = window.localStorage;
if (localStorage) {
localStorage.setItem("mx_hs_url", this.state.hs_url);
localStorage.setItem("mx_user_id", user_id);
localStorage.setItem("mx_access_token", access_token);
} else {
console.warn("No local storage available: can't persist session!");
}
if (this.props.onLoggedIn) {
this.props.onLoggedIn();
}
},
componentForStep: function(step) {
switch (step) {
case 'initial':
return (
<div>
<form onSubmit={this.onInitialStageSubmit}>
Email: <input type="text" ref="email" /><br />
Username: <input type="text" ref="username" /><br />
Password: <input type="password" ref="password" /><br />
<ServerConfig ref="serverConfig" />
<input type="submit" value="Continue" />
</form>
</div>
);
// XXX: clearly these should be separate organisms
case 'stage_m.login.email.identity':
return (
<div>
Please check your email to continue registration.
</div>
);
case 'stage_m.login.recaptcha':
return (
<div ref="recaptchaContainer">
This Home Server would like to make sure you're not a robot
<div id="mx_recaptcha"></div>
</div>
);
}
},
onCaptchaLoaded: function() {
if (this.refs.recaptchaContainer) {
var sitekey = this.authParams['m.login.recaptcha'].public_key;
global.grecaptcha.render('mx_recaptcha', {
'sitekey': sitekey,
'callback': this.onCaptchaDone
});
}
},
onCaptchaDone: function(captcha_response) {
this.tryRegister({
type: 'm.login.recaptcha',
response: captcha_response
});
},
tryRegister: function(auth) {
var self = this;
MatrixClientPeg.get().register(
this.savedParams.username,
this.savedParams.password,
this.authSessionId,
auth
).done(function(result) {
self.onRegistered(result.user_id, result.access_token);
}, function(error) {
if (error.httpStatus == 401 && error.data.flows) {
self.authParams = error.data.params;
self.authSessionId = error.data.session;
self.completedStages = error.data.completed;
var flow = self.chooseFlow(error.data.flows);
var flowStage = self.firstUncompletedStageIndex(flow);
var numDone = self.numCompletedStages(flow);
self.setState({
busy: false,
flows: flow,
currentStep: 1+numDone,
totalSteps: flow.stages.length+1,
flowStage: flowStage
});
self.startStage(flow.stages[flowStage]);
} else {
var errorText = "Unable to contact the given Home Server";
if (error.httpStatus == 401) {
errorText = "Authorisation failed!";
}
self.setStep("initial");
self.setState({
busy: false,
errorText: errorText
});
}
});
},
showLogin: function(ev) {
ev.preventDefault();
dis.dispatch({
action: 'start_login'
});
}
};