mirror of https://github.com/vector-im/riot-web
commit
8ea0117a09
264
src/Signup.js
264
src/Signup.js
|
@ -1,16 +1,272 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
var SignupStages = require("./SignupStages");
|
||||||
var dis = require("./dispatcher");
|
var dis = require("./dispatcher");
|
||||||
var q = require("q");
|
var q = require("q");
|
||||||
|
|
||||||
class Register {
|
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||||
|
|
||||||
}
|
/**
|
||||||
|
* A base class for common functionality between Registration and Login e.g.
|
||||||
class Login {
|
* storage of HS/IS URLs.
|
||||||
|
*/
|
||||||
|
class Signup {
|
||||||
constructor(hsUrl, isUrl) {
|
constructor(hsUrl, isUrl) {
|
||||||
this._hsUrl = hsUrl;
|
this._hsUrl = hsUrl;
|
||||||
this._isUrl = isUrl;
|
this._isUrl = isUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHomeserverUrl() {
|
||||||
|
return this._hsUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentityServerUrl() {
|
||||||
|
return this._isUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHomeserverUrl(hsUrl) {
|
||||||
|
this._hsUrl = hsUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIdentityServerUrl(isUrl) {
|
||||||
|
this._isUrl = isUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration logic class
|
||||||
|
*/
|
||||||
|
class Register extends Signup {
|
||||||
|
constructor(hsUrl, isUrl) {
|
||||||
|
super(hsUrl, isUrl);
|
||||||
|
this.setStep("START");
|
||||||
|
this.data = null; // from the server
|
||||||
|
// random other stuff (e.g. query params, NOT params from the server)
|
||||||
|
this.params = {};
|
||||||
|
this.credentials = null;
|
||||||
|
this.activeStage = null;
|
||||||
|
this.registrationPromise = null;
|
||||||
|
// These values MUST be undefined else we'll send "username: null" which
|
||||||
|
// will error on Synapse rather than having the key absent.
|
||||||
|
this.username = undefined; // desired
|
||||||
|
this.email = undefined; // desired
|
||||||
|
this.password = undefined; // desired
|
||||||
|
}
|
||||||
|
|
||||||
|
setClientSecret(secret) {
|
||||||
|
this.params.clientSecret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionId(sessionId) {
|
||||||
|
this.params.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRegistrationUrl(regUrl) {
|
||||||
|
this.params.registrationUrl = regUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIdSid(idSid) {
|
||||||
|
this.params.idSid = idSid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStep() {
|
||||||
|
return this._step;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCredentials() {
|
||||||
|
return this.credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
getServerData() {
|
||||||
|
return this.data || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getPromise() {
|
||||||
|
return this.registrationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStep(step) {
|
||||||
|
this._step = 'Register.' + step;
|
||||||
|
// TODO:
|
||||||
|
// It's a shame this is going to the global dispatcher, we only really
|
||||||
|
// want things which have an instance of this class to be able to add
|
||||||
|
// listeners...
|
||||||
|
console.log("Dispatching 'registration_step_update' for step %s", this._step);
|
||||||
|
dis.dispatch({
|
||||||
|
action: "registration_step_update"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
register(formVals) {
|
||||||
|
var {username, password, email} = formVals;
|
||||||
|
this.email = email;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
|
||||||
|
// feels a bit wrong to be clobbering the global client for something we
|
||||||
|
// don't even know if it'll work, but we'll leave this here for now to
|
||||||
|
// not complicate matters further. It would be nicer to isolate this
|
||||||
|
// logic entirely from the rest of the app though.
|
||||||
|
MatrixClientPeg.replaceUsingUrls(
|
||||||
|
this._hsUrl,
|
||||||
|
this._isUrl
|
||||||
|
);
|
||||||
|
return this._tryRegister();
|
||||||
|
}
|
||||||
|
|
||||||
|
_tryRegister(authDict) {
|
||||||
|
var self = this;
|
||||||
|
return MatrixClientPeg.get().register(
|
||||||
|
this.username, this.password, this.params.sessionId, authDict
|
||||||
|
).then(function(result) {
|
||||||
|
self.credentials = result;
|
||||||
|
self.setStep("COMPLETE");
|
||||||
|
return result; // contains the credentials
|
||||||
|
}, function(error) {
|
||||||
|
if (error.httpStatus === 401 && error.data && error.data.flows) {
|
||||||
|
self.data = error.data || {};
|
||||||
|
var flow = self.chooseFlow(error.data.flows);
|
||||||
|
|
||||||
|
if (flow) {
|
||||||
|
console.log("Active flow => %s", JSON.stringify(flow));
|
||||||
|
var flowStage = self.firstUncompletedStage(flow);
|
||||||
|
return self.startStage(flowStage);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("Unable to register - missing email address?");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (error.errcode === 'M_USER_IN_USE') {
|
||||||
|
throw new Error("Username in use");
|
||||||
|
} else if (error.httpStatus == 401) {
|
||||||
|
throw new Error("Authorisation failed!");
|
||||||
|
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
||||||
|
throw new Error(`Registration failed! (${error.httpStatus})`);
|
||||||
|
} else if (error.httpStatus >= 500 && error.httpStatus < 600) {
|
||||||
|
throw new Error(
|
||||||
|
`Server error during registration! (${error.httpStatus})`
|
||||||
|
);
|
||||||
|
} else if (error.name == "M_MISSING_PARAM") {
|
||||||
|
// The HS hasn't remembered the login params from
|
||||||
|
// the first try when the login email was sent.
|
||||||
|
throw new Error(
|
||||||
|
"This home server does not support resuming registration."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUncompletedStage(flow) {
|
||||||
|
for (var i = 0; i < flow.stages.length; ++i) {
|
||||||
|
if (!this.hasCompletedStage(flow.stages[i])) {
|
||||||
|
return flow.stages[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCompletedStage(stageType) {
|
||||||
|
var completed = (this.data || {}).completed || [];
|
||||||
|
return completed.indexOf(stageType) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
startStage(stageName) {
|
||||||
|
var self = this;
|
||||||
|
this.setStep(`STEP_${stageName}`);
|
||||||
|
var StageClass = SignupStages[stageName];
|
||||||
|
if (!StageClass) {
|
||||||
|
// no idea how to handle this!
|
||||||
|
throw new Error("Unknown stage: " + stageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stage = new StageClass(MatrixClientPeg.get(), this);
|
||||||
|
this.activeStage = stage;
|
||||||
|
return stage.complete().then(function(request) {
|
||||||
|
if (request.auth) {
|
||||||
|
console.log("Stage %s is returning an auth dict", stageName);
|
||||||
|
return self._tryRegister(request.auth);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// never resolve the promise chain. This is for things like email auth
|
||||||
|
// which display a "check your email" message and relies on the
|
||||||
|
// link in the email to actually register you.
|
||||||
|
console.log("Waiting for external action.");
|
||||||
|
return q.defer().promise;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseFlow(flows) {
|
||||||
|
// If the user gave us an email then we want to pick an email
|
||||||
|
// flow we can do, else any other flow.
|
||||||
|
var emailFlow = null;
|
||||||
|
var otherFlow = null;
|
||||||
|
flows.forEach(function(flow) {
|
||||||
|
var flowHasEmail = false;
|
||||||
|
for (var stageI = 0; stageI < flow.stages.length; ++stageI) {
|
||||||
|
var stage = flow.stages[stageI];
|
||||||
|
|
||||||
|
if (!SignupStages[stage]) {
|
||||||
|
// we can't do this flow, don't have a Stage impl.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stage === EMAIL_STAGE_TYPE) {
|
||||||
|
flowHasEmail = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flowHasEmail) {
|
||||||
|
emailFlow = flow;
|
||||||
|
} else {
|
||||||
|
otherFlow = flow;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.email || this.hasCompletedStage(EMAIL_STAGE_TYPE)) {
|
||||||
|
// we've been given an email or we've already done an email part
|
||||||
|
return emailFlow;
|
||||||
|
} else {
|
||||||
|
return otherFlow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recheckState() {
|
||||||
|
// feels a bit wrong to be clobbering the global client for something we
|
||||||
|
// don't even know if it'll work, but we'll leave this here for now to
|
||||||
|
// not complicate matters further. It would be nicer to isolate this
|
||||||
|
// logic entirely from the rest of the app though.
|
||||||
|
MatrixClientPeg.replaceUsingUrls(
|
||||||
|
this._hsUrl,
|
||||||
|
this._isUrl
|
||||||
|
);
|
||||||
|
// We've been given a bunch of data from a previous register step,
|
||||||
|
// this only happens for email auth currently. It's kinda ming we need
|
||||||
|
// to know this though. A better solution would be to ask the stages if
|
||||||
|
// they are ready to do something rather than accepting that we know about
|
||||||
|
// email auth and its internals.
|
||||||
|
this.params.hasEmailInfo = (
|
||||||
|
this.params.clientSecret && this.params.sessionId && this.params.idSid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.params.hasEmailInfo) {
|
||||||
|
this.registrationPromise = this.startStage(EMAIL_STAGE_TYPE);
|
||||||
|
}
|
||||||
|
return this.registrationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
tellStage(stageName, data) {
|
||||||
|
if (this.activeStage && this.activeStage.type === stageName) {
|
||||||
|
console.log("Telling stage %s about something..", stageName);
|
||||||
|
this.activeStage.onReceiveData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Login extends Signup {
|
||||||
|
constructor(hsUrl, isUrl) {
|
||||||
|
super(hsUrl, isUrl);
|
||||||
this._currentFlowIndex = 0;
|
this._currentFlowIndex = 0;
|
||||||
this._flows = [];
|
this._flows = [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
"use strict";
|
||||||
|
var q = require("q");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface class which login types should abide by.
|
||||||
|
*/
|
||||||
|
class Stage {
|
||||||
|
constructor(type, matrixClient, signupInstance) {
|
||||||
|
this.type = type;
|
||||||
|
this.client = matrixClient;
|
||||||
|
this.signupInstance = signupInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
// Return a promise which is:
|
||||||
|
// RESOLVED => With an Object which has an 'auth' key which is the auth dict
|
||||||
|
// to submit.
|
||||||
|
// REJECTED => With an Error if there was a problem with this stage.
|
||||||
|
// Has a "message" string and an "isFatal" flag.
|
||||||
|
return q.reject("NOT IMPLEMENTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
onReceiveData() {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Stage.TYPE = "NOT IMPLEMENTED";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This stage requires no auth.
|
||||||
|
*/
|
||||||
|
class DummyStage extends Stage {
|
||||||
|
constructor(matrixClient, signupInstance) {
|
||||||
|
super(DummyStage.TYPE, matrixClient, signupInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
return q({
|
||||||
|
auth: {
|
||||||
|
type: DummyStage.TYPE
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DummyStage.TYPE = "m.login.dummy";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This stage uses Google's Recaptcha to do auth.
|
||||||
|
*/
|
||||||
|
class RecaptchaStage extends Stage {
|
||||||
|
constructor(matrixClient, signupInstance) {
|
||||||
|
super(RecaptchaStage.TYPE, matrixClient, signupInstance);
|
||||||
|
this.defer = q.defer(); // resolved with the captcha response
|
||||||
|
this.publicKey = null; // from the HS
|
||||||
|
this.divId = null; // from the UI component
|
||||||
|
}
|
||||||
|
|
||||||
|
// called when the UI component has loaded the recaptcha <div> so we can
|
||||||
|
// render to it.
|
||||||
|
onReceiveData(data) {
|
||||||
|
if (!data || !data.divId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.divId = data.divId;
|
||||||
|
this._attemptRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
var publicKey;
|
||||||
|
var serverParams = this.signupInstance.getServerData().params;
|
||||||
|
if (serverParams && serverParams["m.login.recaptcha"]) {
|
||||||
|
publicKey = serverParams["m.login.recaptcha"].public_key;
|
||||||
|
}
|
||||||
|
if (!publicKey) {
|
||||||
|
return q.reject({
|
||||||
|
message: "This server has not supplied enough information for Recaptcha " +
|
||||||
|
"authentication",
|
||||||
|
isFatal: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
this._attemptRender();
|
||||||
|
return this.defer.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
_attemptRender() {
|
||||||
|
if (!global.grecaptcha) {
|
||||||
|
console.error("grecaptcha not loaded!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.publicKey) {
|
||||||
|
console.error("No public key for recaptcha!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.divId) {
|
||||||
|
console.error("No div ID specified!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Rendering to %s", this.divId);
|
||||||
|
var self = this;
|
||||||
|
global.grecaptcha.render(this.divId, {
|
||||||
|
sitekey: this.publicKey,
|
||||||
|
callback: function(response) {
|
||||||
|
console.log("Received captcha response");
|
||||||
|
self.defer.resolve({
|
||||||
|
auth: {
|
||||||
|
type: 'm.login.recaptcha',
|
||||||
|
response: response
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RecaptchaStage.TYPE = "m.login.recaptcha";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This state uses the IS to verify email addresses.
|
||||||
|
*/
|
||||||
|
class EmailIdentityStage extends Stage {
|
||||||
|
constructor(matrixClient, signupInstance) {
|
||||||
|
super(EmailIdentityStage.TYPE, matrixClient, signupInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
_completeVerify() {
|
||||||
|
// pull out the host of the IS URL by creating an anchor element
|
||||||
|
var isLocation = document.createElement('a');
|
||||||
|
isLocation.href = this.signupInstance.getIdentityServerUrl();
|
||||||
|
|
||||||
|
return q({
|
||||||
|
auth: {
|
||||||
|
type: 'm.login.email.identity',
|
||||||
|
threepid_creds: {
|
||||||
|
sid: this.signupInstance.params.idSid,
|
||||||
|
client_secret: this.signupInstance.params.clientSecret,
|
||||||
|
id_server: isLocation.host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete the email stage.
|
||||||
|
*
|
||||||
|
* This is called twice under different circumstances:
|
||||||
|
* 1) When requesting an email token from the IS
|
||||||
|
* 2) When validating query parameters received from the link in the email
|
||||||
|
*/
|
||||||
|
complete() {
|
||||||
|
// TODO: The Registration class shouldn't really know this info.
|
||||||
|
if (this.signupInstance.params.hasEmailInfo) {
|
||||||
|
return this._completeVerify();
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientSecret = this.client.generateClientSecret();
|
||||||
|
var nextLink = this.signupInstance.params.registrationUrl +
|
||||||
|
'?client_secret=' +
|
||||||
|
encodeURIComponent(clientSecret) +
|
||||||
|
"&hs_url=" +
|
||||||
|
encodeURIComponent(this.signupInstance.getHomeserverUrl()) +
|
||||||
|
"&is_url=" +
|
||||||
|
encodeURIComponent(this.signupInstance.getIdentityServerUrl()) +
|
||||||
|
"&session_id=" +
|
||||||
|
encodeURIComponent(this.signupInstance.getServerData().session);
|
||||||
|
|
||||||
|
return this.client.requestEmailToken(
|
||||||
|
this.signupInstance.email,
|
||||||
|
clientSecret,
|
||||||
|
1, // TODO: Multiple send attempts?
|
||||||
|
nextLink
|
||||||
|
).then(function(response) {
|
||||||
|
return {}; // don't want to make a request
|
||||||
|
}, function(error) {
|
||||||
|
console.error(error);
|
||||||
|
var e = {
|
||||||
|
isFatal: true
|
||||||
|
};
|
||||||
|
if (error.errcode == 'THREEPID_IN_USE') {
|
||||||
|
e.message = "Email in use";
|
||||||
|
} else {
|
||||||
|
e.message = 'Unable to contact the given identity server';
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EmailIdentityStage.TYPE = "m.login.email.identity";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
[DummyStage.TYPE]: DummyStage,
|
||||||
|
[RecaptchaStage.TYPE]: RecaptchaStage,
|
||||||
|
[EmailIdentityStage.TYPE]: EmailIdentityStage
|
||||||
|
};
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
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 DIV_ID = 'mx_recaptcha';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pure UI component which displays a captcha form.
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'CaptchaForm',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
onCaptchaLoaded: React.PropTypes.func.isRequired // called with div id name
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onCaptchaLoaded: function() {
|
||||||
|
console.error("Unhandled onCaptchaLoaded");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
|
||||||
|
// so we do this instead.
|
||||||
|
var self = this;
|
||||||
|
if (this.refs.recaptchaContainer) {
|
||||||
|
console.log("Loading recaptcha script...");
|
||||||
|
var scriptTag = document.createElement('script');
|
||||||
|
window.mx_on_recaptcha_loaded = function() {
|
||||||
|
console.log("Loaded recaptcha script.");
|
||||||
|
self.props.onCaptchaLoaded(DIV_ID);
|
||||||
|
};
|
||||||
|
scriptTag.setAttribute(
|
||||||
|
'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit"
|
||||||
|
);
|
||||||
|
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// FIXME: Tight coupling with the div id and SignupStages.js
|
||||||
|
return (
|
||||||
|
<div ref="recaptchaContainer">
|
||||||
|
This Home Server would like to make sure you are not a robot
|
||||||
|
<div id={DIV_ID}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,352 +0,0 @@
|
||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
FieldErrors: {
|
|
||||||
PasswordMismatch: 'PasswordMismatch',
|
|
||||||
TooShort: 'TooShort',
|
|
||||||
Missing: 'Missing',
|
|
||||||
InUse: 'InUse',
|
|
||||||
Length: 'Length'
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
step: 'initial',
|
|
||||||
busy: false,
|
|
||||||
currentStep: 0,
|
|
||||||
totalSteps: 1
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
|
||||||
this.savedParams = {
|
|
||||||
email: '',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: ''
|
|
||||||
};
|
|
||||||
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', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit");
|
|
||||||
this.refs.recaptchaContainer.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();
|
|
||||||
|
|
||||||
var formVals = this.getRegFormVals();
|
|
||||||
this.savedParams = formVals;
|
|
||||||
|
|
||||||
var badFields = {};
|
|
||||||
if (formVals.password != formVals.confirmPassword) {
|
|
||||||
badFields.confirmPassword = this.FieldErrors.PasswordMismatch;
|
|
||||||
}
|
|
||||||
if (formVals.password == '') {
|
|
||||||
badFields.password = this.FieldErrors.Missing;
|
|
||||||
} else if (formVals.password.length < 6) {
|
|
||||||
badFields.password = this.FieldErrors.Length;
|
|
||||||
}
|
|
||||||
if (formVals.username == '') {
|
|
||||||
badFields.username = this.FieldErrors.Missing;
|
|
||||||
}
|
|
||||||
if (formVals.email == '') {
|
|
||||||
badFields.email = this.FieldErrors.Missing;
|
|
||||||
}
|
|
||||||
if (Object.keys(badFields).length > 0) {
|
|
||||||
this.onBadFields(badFields);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MatrixClientPeg.replaceUsingUrls(
|
|
||||||
this.getHsUrl(),
|
|
||||||
this.getIsUrl()
|
|
||||||
);
|
|
||||||
this.setState({
|
|
||||||
hs_url: this.getHsUrl(),
|
|
||||||
is_url: this.getIsUrl()
|
|
||||||
});
|
|
||||||
this.setState({busy: true});
|
|
||||||
|
|
||||||
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) {
|
|
||||||
console.error(error);
|
|
||||||
self.setStep('initial');
|
|
||||||
var newState = {busy: false};
|
|
||||||
if (error.errcode == 'THREEPID_IN_USE') {
|
|
||||||
self.onBadFields({email: self.FieldErrors.InUse});
|
|
||||||
} else {
|
|
||||||
newState.errorText = 'Unable to contact the given identity server';
|
|
||||||
}
|
|
||||||
self.setState(newState);
|
|
||||||
});
|
|
||||||
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.replaceUsingAccessToken(
|
|
||||||
this.state.hs_url, this.state.is_url, user_id, access_token
|
|
||||||
);
|
|
||||||
if (this.props.onLoggedIn) {
|
|
||||||
this.props.onLoggedIn();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (flow) {
|
|
||||||
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 {
|
|
||||||
self.setState({
|
|
||||||
busy: false,
|
|
||||||
errorText: "Unable to register - missing email address?"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(error);
|
|
||||||
self.setStep("initial");
|
|
||||||
var newState = {
|
|
||||||
busy: false,
|
|
||||||
errorText: "Unable to contact the given Home Server"
|
|
||||||
};
|
|
||||||
if (error.name == 'M_USER_IN_USE') {
|
|
||||||
delete newState.errorText;
|
|
||||||
self.onBadFields({
|
|
||||||
username: self.FieldErrors.InUse
|
|
||||||
});
|
|
||||||
} else if (error.httpStatus == 401) {
|
|
||||||
newState.errorText = "Authorisation failed!";
|
|
||||||
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
|
||||||
newState.errorText = "Registration failed!";
|
|
||||||
} else if (error.httpStatus >= 500 && error.httpStatus < 600) {
|
|
||||||
newState.errorText = "Server error during registration!";
|
|
||||||
} else if (error.name == "M_MISSING_PARAM") {
|
|
||||||
// The HS hasn't remembered the login params from
|
|
||||||
// the first try when the login email was sent.
|
|
||||||
newState.errorText = "This home server does not support resuming registration.";
|
|
||||||
}
|
|
||||||
self.setState(newState);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
showLogin: function(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'start_login'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
Loading…
Reference in New Issue