Skip to content

Commit d84566a

Browse files
mooniondplewis
authored andcommitted
Ajax password reset (#5332)
* adapted public api route for use with ajax * Elegant error handling * Fixed error return * Public API error flow redone, tests * Fixed code to pre-build form * Public API change password return params * Reverted errors in resetPassword * Fixed querystring call * Success test on ajax password reset * Added few more routes to tests for coverage * More tests and redone error return slightly * Updated error text * Console logs removal, renamed test, added {} to if * Wrong error sent * Revert changes * Revert "Revert changes" This reverts commit 68ee2c4. * real revert of {} * nits and test fix * fix tests * throw proper error
1 parent bf033be commit d84566a

File tree

5 files changed

+258
-16
lines changed

5 files changed

+258
-16
lines changed

spec/PasswordPolicy.spec.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,65 @@ describe('Password Policy: ', () => {
913913
});
914914
});
915915

916+
it('Should return error when password violates Password Policy and reset through ajax', async done => {
917+
const user = new Parse.User();
918+
const emailAdapter = {
919+
sendVerificationEmail: () => Promise.resolve(),
920+
sendPasswordResetEmail: async options => {
921+
const response = await request({
922+
url: options.link,
923+
followRedirects: false,
924+
simple: false,
925+
resolveWithFullResponse: true,
926+
});
927+
expect(response.status).toEqual(302);
928+
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
929+
const match = response.text.match(re);
930+
if (!match) {
931+
fail('should have a token');
932+
return;
933+
}
934+
const token = match[1];
935+
936+
try {
937+
await request({
938+
method: 'POST',
939+
url: 'http://localhost:8378/1/apps/test/request_password_reset',
940+
body: `new_password=xuser12&token=${token}&username=user1`,
941+
headers: {
942+
'Content-Type': 'application/x-www-form-urlencoded',
943+
'X-Requested-With': 'XMLHttpRequest',
944+
},
945+
followRedirects: false,
946+
});
947+
} catch (error) {
948+
expect(error.status).not.toBe(302);
949+
expect(error.text).toEqual(
950+
'{"code":-1,"error":"Password cannot contain your username."}'
951+
);
952+
}
953+
await Parse.User.logIn('user1', 'r@nd0m');
954+
done();
955+
},
956+
sendMail: () => {},
957+
};
958+
await reconfigureServer({
959+
appName: 'passwordPolicy',
960+
verifyUserEmails: false,
961+
emailAdapter: emailAdapter,
962+
passwordPolicy: {
963+
doNotAllowUsername: true,
964+
},
965+
publicServerURL: 'http://localhost:8378/1',
966+
});
967+
user.setUsername('user1');
968+
user.setPassword('r@nd0m');
969+
user.set('email', '[email protected]');
970+
await user.signUp();
971+
972+
await Parse.User.requestPasswordReset('[email protected]');
973+
});
974+
916975
it('should reset password even if the new password contains user name while the policy allows', done => {
917976
const user = new Parse.User();
918977
const emailAdapter = {

spec/PublicAPI.spec.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,72 @@ const request = function(url, callback) {
77
};
88

99
describe('public API', () => {
10+
it('should return missing username error on ajax request without username provided', async () => {
11+
await reconfigureServer({
12+
publicServerURL: 'http://localhost:8378/1',
13+
});
14+
15+
try {
16+
await req({
17+
method: 'POST',
18+
url: 'http://localhost:8378/1/apps/test/request_password_reset',
19+
body: `new_password=user1&token=43634643&username=`,
20+
headers: {
21+
'Content-Type': 'application/x-www-form-urlencoded',
22+
'X-Requested-With': 'XMLHttpRequest',
23+
},
24+
followRedirects: false,
25+
});
26+
} catch (error) {
27+
expect(error.status).not.toBe(302);
28+
expect(error.text).toEqual('{"code":200,"error":"Missing username"}');
29+
}
30+
});
31+
32+
it('should return missing token error on ajax request without token provided', async () => {
33+
await reconfigureServer({
34+
publicServerURL: 'http://localhost:8378/1',
35+
});
36+
37+
try {
38+
await req({
39+
method: 'POST',
40+
url: 'http://localhost:8378/1/apps/test/request_password_reset',
41+
body: `new_password=user1&token=&username=Johnny`,
42+
headers: {
43+
'Content-Type': 'application/x-www-form-urlencoded',
44+
'X-Requested-With': 'XMLHttpRequest',
45+
},
46+
followRedirects: false,
47+
});
48+
} catch (error) {
49+
expect(error.status).not.toBe(302);
50+
expect(error.text).toEqual('{"code":-1,"error":"Missing token"}');
51+
}
52+
});
53+
54+
it('should return missing password error on ajax request without password provided', async () => {
55+
await reconfigureServer({
56+
publicServerURL: 'http://localhost:8378/1',
57+
});
58+
59+
try {
60+
await req({
61+
method: 'POST',
62+
url: 'http://localhost:8378/1/apps/test/request_password_reset',
63+
body: `new_password=&token=132414&username=Johnny`,
64+
headers: {
65+
'Content-Type': 'application/x-www-form-urlencoded',
66+
'X-Requested-With': 'XMLHttpRequest',
67+
},
68+
followRedirects: false,
69+
});
70+
} catch (error) {
71+
expect(error.status).not.toBe(302);
72+
expect(error.text).toEqual('{"code":201,"error":"Missing password"}');
73+
}
74+
});
75+
1076
it('should get invalid_link.html', done => {
1177
request(
1278
'http://localhost:8378/1/apps/invalid_link.html',

spec/ValidationAndPasswordsReset.spec.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,89 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
910910
});
911911
});
912912

913+
it('should programmatically reset password on ajax request', async done => {
914+
const user = new Parse.User();
915+
const emailAdapter = {
916+
sendVerificationEmail: () => Promise.resolve(),
917+
sendPasswordResetEmail: async options => {
918+
const response = await request({
919+
url: options.link,
920+
followRedirects: false,
921+
});
922+
expect(response.status).toEqual(302);
923+
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/;
924+
const match = response.text.match(re);
925+
if (!match) {
926+
fail('should have a token');
927+
return;
928+
}
929+
const token = match[1];
930+
931+
const resetResponse = await request({
932+
url: 'http://localhost:8378/1/apps/test/request_password_reset',
933+
method: 'POST',
934+
body: { new_password: 'hello', token, username: 'zxcv' },
935+
headers: {
936+
'Content-Type': 'application/x-www-form-urlencoded',
937+
'X-Requested-With': 'XMLHttpRequest',
938+
},
939+
followRedirects: false,
940+
});
941+
expect(resetResponse.status).toEqual(200);
942+
expect(resetResponse.text).toEqual('"Password successfully reset"');
943+
944+
await Parse.User.logIn('zxcv', 'hello');
945+
const config = Config.get('test');
946+
const results = await config.database.adapter.find(
947+
'_User',
948+
{ fields: {} },
949+
{ username: 'zxcv' },
950+
{ limit: 1 }
951+
);
952+
// _perishable_token should be unset after reset password
953+
expect(results.length).toEqual(1);
954+
expect(results[0]['_perishable_token']).toEqual(undefined);
955+
done();
956+
},
957+
sendMail: () => {},
958+
};
959+
await reconfigureServer({
960+
appName: 'emailing app',
961+
verifyUserEmails: true,
962+
emailAdapter: emailAdapter,
963+
publicServerURL: 'http://localhost:8378/1',
964+
});
965+
user.setPassword('asdf');
966+
user.setUsername('zxcv');
967+
user.set('email', '[email protected]');
968+
await user.signUp();
969+
await Parse.User.requestPasswordReset('[email protected]');
970+
});
971+
972+
it('should return ajax failure error on ajax request with wrong data provided', async () => {
973+
await reconfigureServer({
974+
publicServerURL: 'http://localhost:8378/1',
975+
});
976+
977+
try {
978+
await request({
979+
method: 'POST',
980+
url: 'http://localhost:8378/1/apps/test/request_password_reset',
981+
body: `new_password=user1&token=12345&username=Johnny`,
982+
headers: {
983+
'Content-Type': 'application/x-www-form-urlencoded',
984+
'X-Requested-With': 'XMLHttpRequest',
985+
},
986+
followRedirects: false,
987+
});
988+
} catch (error) {
989+
expect(error.status).not.toBe(302);
990+
expect(error.text).toEqual(
991+
'{"code":-1,"error":"Failed to reset password: username / email / token is invalid"}'
992+
);
993+
}
994+
});
995+
913996
it('deletes password reset token on email address change', done => {
914997
reconfigureServer({
915998
appName: 'coolapp',

src/Controllers/UserController.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class UserController extends AdaptableController {
9090
)
9191
.then(results => {
9292
if (results.length != 1) {
93-
throw undefined;
93+
throw 'Failed to reset password: username / email / token is invalid';
9494
}
9595

9696
if (
@@ -246,7 +246,7 @@ export class UserController extends AdaptableController {
246246
return this.checkResetTokenValidity(username, token)
247247
.then(user => updateUserPassword(user.objectId, password, this.config))
248248
.catch(error => {
249-
if (error.message) {
249+
if (error && error.message) {
250250
// in case of Parse.Error, fail with the error message only
251251
return Promise.reject(error.message);
252252
} else {

src/Routers/PublicAPIRouter.js

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import express from 'express';
44
import path from 'path';
55
import fs from 'fs';
66
import qs from 'querystring';
7+
import { Parse } from 'parse/node';
78

89
const public_html = path.resolve(__dirname, '../../public_html');
910
const views = path.resolve(__dirname, '../../views');
@@ -159,34 +160,67 @@ export class PublicAPIRouter extends PromiseRouter {
159160

160161
const { username, token, new_password } = req.body;
161162

162-
if (!username || !token || !new_password) {
163+
if ((!username || !token || !new_password) && req.xhr === false) {
163164
return this.invalidLink(req);
164165
}
165166

167+
if (!username) {
168+
throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'Missing username');
169+
}
170+
171+
if (!token) {
172+
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing token');
173+
}
174+
175+
if (!new_password) {
176+
throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'Missing password');
177+
}
178+
166179
return config.userController
167180
.updatePassword(username, token, new_password)
168181
.then(
169182
() => {
170-
const params = qs.stringify({ username: username });
171183
return Promise.resolve({
172-
status: 302,
173-
location: `${config.passwordResetSuccessURL}?${params}`,
184+
success: true,
174185
});
175186
},
176187
err => {
177-
const params = qs.stringify({
178-
username: username,
179-
token: token,
180-
id: config.applicationId,
181-
error: err,
182-
app: config.appName,
183-
});
184188
return Promise.resolve({
185-
status: 302,
186-
location: `${config.choosePasswordURL}?${params}`,
189+
success: false,
190+
err,
187191
});
188192
}
189-
);
193+
)
194+
.then(result => {
195+
const params = qs.stringify({
196+
username: username,
197+
token: token,
198+
id: config.applicationId,
199+
error: result.err,
200+
app: config.appName,
201+
});
202+
203+
if (req.xhr) {
204+
if (result.success) {
205+
return Promise.resolve({
206+
status: 200,
207+
response: 'Password successfully reset',
208+
});
209+
}
210+
if (result.err) {
211+
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${result.err}`);
212+
}
213+
}
214+
215+
return Promise.resolve({
216+
status: 302,
217+
location: `${
218+
result.success
219+
? `${config.passwordResetSuccessURL}?username=${username}`
220+
: `${config.choosePasswordURL}?${params}`
221+
}`,
222+
});
223+
});
190224
}
191225

192226
invalidLink(req) {

0 commit comments

Comments
 (0)