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 68ee2c44bf2411ca8b56b039a4d490a7e2f99ae9.
* real revert of {}
* nits and test fix
* fix tests
* throw proper error
This commit is contained in:
@@ -913,6 +913,65 @@ describe('Password Policy: ', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should return error when password violates Password Policy and reset through ajax', async done => {
|
||||||
|
const user = new Parse.User();
|
||||||
|
const emailAdapter = {
|
||||||
|
sendVerificationEmail: () => Promise.resolve(),
|
||||||
|
sendPasswordResetEmail: async options => {
|
||||||
|
const response = await request({
|
||||||
|
url: options.link,
|
||||||
|
followRedirects: false,
|
||||||
|
simple: false,
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
});
|
||||||
|
expect(response.status).toEqual(302);
|
||||||
|
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||||
|
const match = response.text.match(re);
|
||||||
|
if (!match) {
|
||||||
|
fail('should have a token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = match[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await request({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||||
|
body: `new_password=xuser12&token=${token}&username=user1`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
followRedirects: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.status).not.toBe(302);
|
||||||
|
expect(error.text).toEqual(
|
||||||
|
'{"code":-1,"error":"Password cannot contain your username."}'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await Parse.User.logIn('user1', 'r@nd0m');
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
sendMail: () => {},
|
||||||
|
};
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'passwordPolicy',
|
||||||
|
verifyUserEmails: false,
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
passwordPolicy: {
|
||||||
|
doNotAllowUsername: true,
|
||||||
|
},
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
user.setUsername('user1');
|
||||||
|
user.setPassword('r@nd0m');
|
||||||
|
user.set('email', 'user1@parse.com');
|
||||||
|
await user.signUp();
|
||||||
|
|
||||||
|
await Parse.User.requestPasswordReset('user1@parse.com');
|
||||||
|
});
|
||||||
|
|
||||||
it('should reset password even if the new password contains user name while the policy allows', done => {
|
it('should reset password even if the new password contains user name while the policy allows', done => {
|
||||||
const user = new Parse.User();
|
const user = new Parse.User();
|
||||||
const emailAdapter = {
|
const emailAdapter = {
|
||||||
|
|||||||
@@ -7,6 +7,72 @@ const request = function(url, callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('public API', () => {
|
describe('public API', () => {
|
||||||
|
it('should return missing username error on ajax request without username provided', async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await req({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||||
|
body: `new_password=user1&token=43634643&username=`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
followRedirects: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.status).not.toBe(302);
|
||||||
|
expect(error.text).toEqual('{"code":200,"error":"Missing username"}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return missing token error on ajax request without token provided', async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await req({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||||
|
body: `new_password=user1&token=&username=Johnny`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
followRedirects: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.status).not.toBe(302);
|
||||||
|
expect(error.text).toEqual('{"code":-1,"error":"Missing token"}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return missing password error on ajax request without password provided', async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await req({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||||
|
body: `new_password=&token=132414&username=Johnny`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
followRedirects: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.status).not.toBe(302);
|
||||||
|
expect(error.text).toEqual('{"code":201,"error":"Missing password"}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should get invalid_link.html', done => {
|
it('should get invalid_link.html', done => {
|
||||||
request(
|
request(
|
||||||
'http://localhost:8378/1/apps/invalid_link.html',
|
'http://localhost:8378/1/apps/invalid_link.html',
|
||||||
|
|||||||
@@ -910,6 +910,89 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should programmatically reset password on ajax request', async done => {
|
||||||
|
const user = new Parse.User();
|
||||||
|
const emailAdapter = {
|
||||||
|
sendVerificationEmail: () => Promise.resolve(),
|
||||||
|
sendPasswordResetEmail: async options => {
|
||||||
|
const response = await request({
|
||||||
|
url: options.link,
|
||||||
|
followRedirects: false,
|
||||||
|
});
|
||||||
|
expect(response.status).toEqual(302);
|
||||||
|
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/;
|
||||||
|
const match = response.text.match(re);
|
||||||
|
if (!match) {
|
||||||
|
fail('should have a token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = match[1];
|
||||||
|
|
||||||
|
const resetResponse = await request({
|
||||||
|
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||||
|
method: 'POST',
|
||||||
|
body: { new_password: 'hello', token, username: 'zxcv' },
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
followRedirects: false,
|
||||||
|
});
|
||||||
|
expect(resetResponse.status).toEqual(200);
|
||||||
|
expect(resetResponse.text).toEqual('"Password successfully reset"');
|
||||||
|
|
||||||
|
await Parse.User.logIn('zxcv', 'hello');
|
||||||
|
const config = Config.get('test');
|
||||||
|
const results = await config.database.adapter.find(
|
||||||
|
'_User',
|
||||||
|
{ fields: {} },
|
||||||
|
{ username: 'zxcv' },
|
||||||
|
{ limit: 1 }
|
||||||
|
);
|
||||||
|
// _perishable_token should be unset after reset password
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0]['_perishable_token']).toEqual(undefined);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
sendMail: () => {},
|
||||||
|
};
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'emailing app',
|
||||||
|
verifyUserEmails: true,
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
user.setPassword('asdf');
|
||||||
|
user.setUsername('zxcv');
|
||||||
|
user.set('email', 'user@parse.com');
|
||||||
|
await user.signUp();
|
||||||
|
await Parse.User.requestPasswordReset('user@parse.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ajax failure error on ajax request with wrong data provided', async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await request({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||||
|
body: `new_password=user1&token=12345&username=Johnny`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
followRedirects: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.status).not.toBe(302);
|
||||||
|
expect(error.text).toEqual(
|
||||||
|
'{"code":-1,"error":"Failed to reset password: username / email / token is invalid"}'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('deletes password reset token on email address change', done => {
|
it('deletes password reset token on email address change', done => {
|
||||||
reconfigureServer({
|
reconfigureServer({
|
||||||
appName: 'coolapp',
|
appName: 'coolapp',
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export class UserController extends AdaptableController {
|
|||||||
)
|
)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
if (results.length != 1) {
|
if (results.length != 1) {
|
||||||
throw undefined;
|
throw 'Failed to reset password: username / email / token is invalid';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -246,7 +246,7 @@ export class UserController extends AdaptableController {
|
|||||||
return this.checkResetTokenValidity(username, token)
|
return this.checkResetTokenValidity(username, token)
|
||||||
.then(user => updateUserPassword(user.objectId, password, this.config))
|
.then(user => updateUserPassword(user.objectId, password, this.config))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error.message) {
|
if (error && error.message) {
|
||||||
// in case of Parse.Error, fail with the error message only
|
// in case of Parse.Error, fail with the error message only
|
||||||
return Promise.reject(error.message);
|
return Promise.reject(error.message);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import express from 'express';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import qs from 'querystring';
|
import qs from 'querystring';
|
||||||
|
import { Parse } from 'parse/node';
|
||||||
|
|
||||||
const public_html = path.resolve(__dirname, '../../public_html');
|
const public_html = path.resolve(__dirname, '../../public_html');
|
||||||
const views = path.resolve(__dirname, '../../views');
|
const views = path.resolve(__dirname, '../../views');
|
||||||
@@ -159,34 +160,67 @@ export class PublicAPIRouter extends PromiseRouter {
|
|||||||
|
|
||||||
const { username, token, new_password } = req.body;
|
const { username, token, new_password } = req.body;
|
||||||
|
|
||||||
if (!username || !token || !new_password) {
|
if ((!username || !token || !new_password) && req.xhr === false) {
|
||||||
return this.invalidLink(req);
|
return this.invalidLink(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'Missing username');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!new_password) {
|
||||||
|
throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'Missing password');
|
||||||
|
}
|
||||||
|
|
||||||
return config.userController
|
return config.userController
|
||||||
.updatePassword(username, token, new_password)
|
.updatePassword(username, token, new_password)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
const params = qs.stringify({ username: username });
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
status: 302,
|
success: true,
|
||||||
location: `${config.passwordResetSuccessURL}?${params}`,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
const params = qs.stringify({
|
|
||||||
username: username,
|
|
||||||
token: token,
|
|
||||||
id: config.applicationId,
|
|
||||||
error: err,
|
|
||||||
app: config.appName,
|
|
||||||
});
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
status: 302,
|
success: false,
|
||||||
location: `${config.choosePasswordURL}?${params}`,
|
err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
.then(result => {
|
||||||
|
const params = qs.stringify({
|
||||||
|
username: username,
|
||||||
|
token: token,
|
||||||
|
id: config.applicationId,
|
||||||
|
error: result.err,
|
||||||
|
app: config.appName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.xhr) {
|
||||||
|
if (result.success) {
|
||||||
|
return Promise.resolve({
|
||||||
|
status: 200,
|
||||||
|
response: 'Password successfully reset',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (result.err) {
|
||||||
|
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${result.err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
status: 302,
|
||||||
|
location: `${
|
||||||
|
result.success
|
||||||
|
? `${config.passwordResetSuccessURL}?username=${username}`
|
||||||
|
: `${config.choosePasswordURL}?${params}`
|
||||||
|
}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidLink(req) {
|
invalidLink(req) {
|
||||||
|
|||||||
Reference in New Issue
Block a user