Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit c46bae5

Browse files
authored
Merge branch 'develop' into travis/cors-on-join
2 parents 7e817f4 + c553323 commit c46bae5

File tree

28 files changed

+2331
-244
lines changed

28 files changed

+2331
-244
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@
9696
"text-encoding-utf-8": "^1.0.1",
9797
"url": "^0.11.0",
9898
"velocity-vector": "github:vector-im/velocity#059e3b2",
99-
"whatwg-fetch": "^1.1.1"
99+
"whatwg-fetch": "^1.1.1",
100+
"zxcvbn": "^4.4.2"
100101
},
101102
"devDependencies": {
102103
"babel-cli": "^6.26.0",

res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,26 @@ limitations under the License.
1919
padding: 20px
2020
}
2121

22+
.mx_CreateKeyBackupDialog_primaryContainer::after {
23+
content: "";
24+
clear: both;
25+
display: block;
26+
}
27+
28+
.mx_CreateKeyBackupDialog_passPhraseHelp {
29+
float: right;
30+
width: 230px;
31+
height: 85px;
32+
margin-left: 20px;
33+
font-size: 80%;
34+
}
35+
36+
.mx_CreateKeyBackupDialog_passPhraseHelp progress {
37+
width: 100%;
38+
}
39+
2240
.mx_CreateKeyBackupDialog_passPhraseInput {
23-
width: 300px;
41+
width: 250px;
2442
border: 1px solid $accent-color;
2543
border-radius: 5px;
2644
padding: 10px;

src/ContentMessages.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,9 +377,9 @@ class ContentMessages {
377377
}
378378
}
379379
if (error) {
380-
dis.dispatch({action: 'upload_failed', upload: upload});
380+
dis.dispatch({action: 'upload_failed', upload, error});
381381
} else {
382-
dis.dispatch({action: 'upload_finished', upload: upload});
382+
dis.dispatch({action: 'upload_finished', upload});
383383
}
384384
});
385385
}

src/Lifecycle.js

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import Modal from './Modal';
3232
import sdk from './index';
3333
import ActiveWidgetStore from './stores/ActiveWidgetStore';
3434
import PlatformPeg from "./PlatformPeg";
35+
import {sendLoginRequest} from "./Login";
3536

3637
/**
3738
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@@ -129,27 +130,17 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
129130
return Promise.resolve(false);
130131
}
131132

132-
// create a temporary MatrixClient to do the login
133-
const client = Matrix.createClient({
134-
baseUrl: queryParams.homeserver,
135-
});
136-
137-
return client.login(
133+
return sendLoginRequest(
134+
queryParams.homeserver,
135+
queryParams.identityServer,
138136
"m.login.token", {
139137
token: queryParams.loginToken,
140138
initial_device_display_name: defaultDeviceDisplayName,
141139
},
142-
).then(function(data) {
140+
).then(function(creds) {
143141
console.log("Logged in with token");
144142
return _clearStorage().then(() => {
145-
_persistCredentialsToLocalStorage({
146-
userId: data.user_id,
147-
deviceId: data.device_id,
148-
accessToken: data.access_token,
149-
homeserverUrl: queryParams.homeserver,
150-
identityServerUrl: queryParams.identityServer,
151-
guest: false,
152-
});
143+
_persistCredentialsToLocalStorage(creds);
153144
return true;
154145
});
155146
}).catch((err) => {

src/Login.js

Lines changed: 36 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
Copyright 2015, 2016 OpenMarket Ltd
33
Copyright 2017 Vector Creations Ltd
4+
Copyright 2018 New Vector Ltd
45
56
Licensed under the Apache License, Version 2.0 (the "License");
67
you may not use this file except in compliance with the License.
@@ -17,7 +18,6 @@ limitations under the License.
1718

1819
import Matrix from "matrix-js-sdk";
1920

20-
import Promise from 'bluebird';
2121
import url from 'url';
2222

2323
export default class Login {
@@ -141,83 +141,27 @@ export default class Login {
141141
};
142142
Object.assign(loginParams, legacyParams);
143143

144-
const client = this._createTemporaryClient();
145-
146144
const tryFallbackHs = (originalError) => {
147-
const fbClient = Matrix.createClient({
148-
baseUrl: self._fallbackHsUrl,
149-
idBaseUrl: this._isUrl,
150-
});
151-
152-
return fbClient.login('m.login.password', loginParams).then(function(data) {
153-
return Promise.resolve({
154-
homeserverUrl: self._fallbackHsUrl,
155-
identityServerUrl: self._isUrl,
156-
userId: data.user_id,
157-
deviceId: data.device_id,
158-
accessToken: data.access_token,
159-
});
160-
}).catch((fallback_error) => {
145+
return sendLoginRequest(
146+
self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams,
147+
).catch((fallback_error) => {
161148
console.log("fallback HS login failed", fallback_error);
162149
// throw the original error
163150
throw originalError;
164151
});
165152
};
166-
const tryLowercaseUsername = (originalError) => {
167-
const loginParamsLowercase = Object.assign({}, loginParams, {
168-
user: username.toLowerCase(),
169-
identifier: {
170-
user: username.toLowerCase(),
171-
},
172-
});
173-
return client.login('m.login.password', loginParamsLowercase).then(function(data) {
174-
return Promise.resolve({
175-
homeserverUrl: self._hsUrl,
176-
identityServerUrl: self._isUrl,
177-
userId: data.user_id,
178-
deviceId: data.device_id,
179-
accessToken: data.access_token,
180-
});
181-
}).catch((fallback_error) => {
182-
console.log("Lowercase username login failed", fallback_error);
183-
// throw the original error
184-
throw originalError;
185-
});
186-
};
187153

188154
let originalLoginError = null;
189-
return client.login('m.login.password', loginParams).then(function(data) {
190-
return Promise.resolve({
191-
homeserverUrl: self._hsUrl,
192-
identityServerUrl: self._isUrl,
193-
userId: data.user_id,
194-
deviceId: data.device_id,
195-
accessToken: data.access_token,
196-
});
197-
}).catch((error) => {
155+
return sendLoginRequest(
156+
self._hsUrl, self._isUrl, 'm.login.password', loginParams,
157+
).catch((error) => {
198158
originalLoginError = error;
199159
if (error.httpStatus === 403) {
200160
if (self._fallbackHsUrl) {
201161
return tryFallbackHs(originalLoginError);
202162
}
203163
}
204164
throw originalLoginError;
205-
}).catch((error) => {
206-
// We apparently squash case at login serverside these days:
207-
// https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475
208-
// so this wasn't needed after all. Keeping the code around in case the
209-
// the situation changes...
210-
211-
/*
212-
if (
213-
error.httpStatus === 403 &&
214-
loginParams.identifier.type === 'm.id.user' &&
215-
username.search(/[A-Z]/) > -1
216-
) {
217-
return tryLowercaseUsername(originalLoginError);
218-
}
219-
*/
220-
throw originalLoginError;
221165
}).catch((error) => {
222166
console.log("Login failed", error);
223167
throw error;
@@ -239,3 +183,32 @@ export default class Login {
239183
return client.getSsoLoginUrl(url.format(parsedUrl), loginType);
240184
}
241185
}
186+
187+
188+
/**
189+
* Send a login request to the given server, and format the response
190+
* as a MatrixClientCreds
191+
*
192+
* @param {string} hsUrl the base url of the Homeserver used to log in.
193+
* @param {string} isUrl the base url of the default identity server
194+
* @param {string} loginType the type of login to do
195+
* @param {object} loginParams the parameters for the login
196+
*
197+
* @returns {MatrixClientCreds}
198+
*/
199+
export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
200+
const client = Matrix.createClient({
201+
baseUrl: hsUrl,
202+
idBaseUrl: isUrl,
203+
});
204+
205+
const data = await client.login(loginType, loginParams);
206+
207+
return {
208+
homeserverUrl: hsUrl,
209+
identityServerUrl: isUrl,
210+
userId: data.user_id,
211+
deviceId: data.device_id,
212+
accessToken: data.access_token,
213+
};
214+
}

src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
import React from 'react';
1818
import sdk from '../../../../index';
1919
import MatrixClientPeg from '../../../../MatrixClientPeg';
20+
import { scorePassword } from '../../../../utils/PasswordScorer';
2021

2122
import FileSaver from 'file-saver';
2223

@@ -30,6 +31,8 @@ const PHASE_BACKINGUP = 4;
3031
const PHASE_DONE = 5;
3132
const PHASE_OPTOUT_CONFIRM = 6;
3233

34+
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
35+
3336
// XXX: copied from ShareDialog: factor out into utils
3437
function selectText(target) {
3538
const range = document.createRange();
@@ -52,6 +55,8 @@ export default React.createClass({
5255
passPhraseConfirm: '',
5356
copied: false,
5457
downloaded: false,
58+
zxcvbnResult: null,
59+
setPassPhrase: false,
5560
};
5661
},
5762

@@ -128,6 +133,7 @@ export default React.createClass({
128133
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
129134
this.setState({
130135
copied: false,
136+
downloaded: false,
131137
phase: PHASE_SHOWKEY,
132138
});
133139
},
@@ -145,7 +151,9 @@ export default React.createClass({
145151
_onPassPhraseConfirmNextClick: async function() {
146152
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
147153
this.setState({
154+
setPassPhrase: true,
148155
copied: false,
156+
downloaded: false,
149157
phase: PHASE_SHOWKEY,
150158
});
151159
},
@@ -173,6 +181,10 @@ export default React.createClass({
173181
_onPassPhraseChange: function(e) {
174182
this.setState({
175183
passPhrase: e.target.value,
184+
// precompute this and keep it in state: zxcvbn is fast but
185+
// we use it in a couple of different places so no point recomputing
186+
// it unnecessarily.
187+
zxcvbnResult: scorePassword(e.target.value),
176188
});
177189
},
178190

@@ -183,17 +195,46 @@ export default React.createClass({
183195
},
184196

185197
_passPhraseIsValid: function() {
186-
return this.state.passPhrase !== '';
198+
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
187199
},
188200

189201
_renderPhasePassPhrase: function() {
190202
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
191203
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
204+
205+
let strengthMeter;
206+
let helpText;
207+
if (this.state.zxcvbnResult) {
208+
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
209+
helpText = _t("Great! This passphrase looks strong enough.");
210+
} else {
211+
const suggestions = [];
212+
for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
213+
suggestions.push(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
214+
}
215+
const suggestionBlock = suggestions.length > 0 ? <div>
216+
{suggestions}
217+
</div> : null;
218+
219+
helpText = <div>
220+
{this.state.zxcvbnResult.feedback.warning}
221+
{suggestionBlock}
222+
</div>;
223+
}
224+
strengthMeter = <div>
225+
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
226+
</div>;
227+
}
228+
192229
return <div>
193230
<p>{_t("Secure your encrypted message history with a Recovery Passphrase.")}</p>
194231
<p>{_t("You'll need it if you log out or lose access to this device.")}</p>
195232

196233
<div className="mx_CreateKeyBackupDialog_primaryContainer">
234+
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
235+
{strengthMeter}
236+
{helpText}
237+
</div>
197238
<input type="password"
198239
onChange={this._onPassPhraseChange}
199240
onKeyPress={this._onPassPhraseKeyPress}
@@ -290,9 +331,17 @@ export default React.createClass({
290331

291332
_renderPhaseShowKey: function() {
292333
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
334+
335+
let bodyText;
336+
if (this.state.setPassPhrase) {
337+
bodyText = _t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.");
338+
} else {
339+
bodyText = _t("As a safety net, you can use it to restore your encrypted message history.");
340+
}
341+
293342
return <div>
294343
<p>{_t("Make a copy of this Recovery Key and keep it safe.")}</p>
295-
<p>{_t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.")}</p>
344+
<p>{bodyText}</p>
296345
<p className="mx_CreateKeyBackupDialog_primaryContainer">
297346
<div>{_t("Your Recovery Key")}</div>
298347
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">

src/components/structures/GroupView.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ export default React.createClass({
470470
GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit));
471471
let willDoOnboarding = false;
472472
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
473-
GroupStore.on('error', (err, errorGroupId) => {
473+
GroupStore.on('error', (err, errorGroupId, stateKey) => {
474474
if (this._unmounted || groupId !== errorGroupId) return;
475475
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
476476
dis.dispatch({
@@ -483,11 +483,13 @@ export default React.createClass({
483483
dis.dispatch({action: 'require_registration'});
484484
willDoOnboarding = true;
485485
}
486-
this.setState({
487-
summary: null,
488-
error: err,
489-
editing: false,
490-
});
486+
if (stateKey === GroupStore.STATE_KEY.Summary) {
487+
this.setState({
488+
summary: null,
489+
error: err,
490+
editing: false,
491+
});
492+
}
491493
});
492494
},
493495

@@ -511,7 +513,6 @@ export default React.createClass({
511513
isUserMember: GroupStore.getGroupMembers(this.props.groupId).some(
512514
(m) => m.userId === this._matrixClient.credentials.userId,
513515
),
514-
error: null,
515516
});
516517
// XXX: This might not work but this.props.groupIsNew unused anyway
517518
if (this.props.groupIsNew && firstInit) {
@@ -1157,7 +1158,7 @@ export default React.createClass({
11571158

11581159
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
11591160
return <Spinner />;
1160-
} else if (this.state.summary) {
1161+
} else if (this.state.summary && !this.state.error) {
11611162
const summary = this.state.summary;
11621163

11631164
let avatarNode;

0 commit comments

Comments
 (0)