Skip to content

Commit e81cad0

Browse files
authored
Merge branch 'alpha' into masterKeysIPS
2 parents 8f4fa5d + e6bd2ba commit e81cad0

18 files changed

+382
-141
lines changed

changelogs/CHANGELOG_alpha.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# [6.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.1...6.3.0-alpha.2) (2023-06-20)
2+
3+
4+
### Features
5+
6+
* Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions ([#8425](https://github.com/parse-community/parse-server/issues/8425)) ([44acd6d](https://github.com/parse-community/parse-server/commit/44acd6d9ed157ad4842200c9d01f9c77a05fec3a))
7+
18
# [6.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.3.0-alpha.1) (2023-06-18)
29

310

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parse-server",
3-
"version": "6.3.0-alpha.1",
3+
"version": "6.3.0-alpha.2",
44
"description": "An express module providing a Parse-compatible API server",
55
"main": "lib/index.js",
66
"repository": {

postinstall.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const pkg = require('./package.json');
22

3-
const version = parseFloat(process.version.substr(1));
3+
const version = parseFloat(process.version.substring(1));
44
const minimum = parseFloat(pkg.engines.node.match(/\d+/g).join('.'));
55

66
module.exports = function () {

resources/buildConfigDefinitions.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,16 @@ function inject(t, list) {
255255
props.push(t.objectProperty(t.stringLiteral('action'), action));
256256
}
257257
if (elt.defaultValue) {
258-
const parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
258+
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
259+
if (!parsedValue) {
260+
for (const type of elt.typeAnnotation.types) {
261+
elt.type = type.type;
262+
parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
263+
if (parsedValue) {
264+
break;
265+
}
266+
}
267+
}
259268
if (parsedValue) {
260269
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
261270
} else {

spec/EmailVerificationToken.spec.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,184 @@ describe('Email Verification Token Expiration: ', () => {
288288
});
289289
});
290290

291+
it('can conditionally send emails', async () => {
292+
let sendEmailOptions;
293+
const emailAdapter = {
294+
sendVerificationEmail: options => {
295+
sendEmailOptions = options;
296+
},
297+
sendPasswordResetEmail: () => Promise.resolve(),
298+
sendMail: () => {},
299+
};
300+
const verifyUserEmails = {
301+
method(req) {
302+
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']);
303+
return false;
304+
},
305+
};
306+
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
307+
await reconfigureServer({
308+
appName: 'emailVerifyToken',
309+
verifyUserEmails: verifyUserEmails.method,
310+
emailAdapter: emailAdapter,
311+
emailVerifyTokenValidityDuration: 5, // 5 seconds
312+
publicServerURL: 'http://localhost:8378/1',
313+
});
314+
const beforeSave = {
315+
method(req) {
316+
req.object.set('emailVerified', true);
317+
},
318+
};
319+
const saveSpy = spyOn(beforeSave, 'method').and.callThrough();
320+
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
321+
Parse.Cloud.beforeSave(Parse.User, beforeSave.method);
322+
const user = new Parse.User();
323+
user.setUsername('sets_email_verify_token_expires_at');
324+
user.setPassword('expiringToken');
325+
user.set('email', '[email protected]');
326+
await user.signUp();
327+
328+
const config = Config.get('test');
329+
const results = await config.database.find(
330+
'_User',
331+
{
332+
username: 'sets_email_verify_token_expires_at',
333+
},
334+
{},
335+
Auth.maintenance(config)
336+
);
337+
338+
expect(results.length).toBe(1);
339+
const user_data = results[0];
340+
expect(typeof user_data).toBe('object');
341+
expect(user_data.emailVerified).toEqual(true);
342+
expect(user_data._email_verify_token).toBeUndefined();
343+
expect(user_data._email_verify_token_expires_at).toBeUndefined();
344+
expect(emailSpy).not.toHaveBeenCalled();
345+
expect(saveSpy).toHaveBeenCalled();
346+
expect(sendEmailOptions).toBeUndefined();
347+
expect(verifySpy).toHaveBeenCalled();
348+
});
349+
350+
it('can conditionally send emails and allow conditional login', async () => {
351+
let sendEmailOptions;
352+
const emailAdapter = {
353+
sendVerificationEmail: options => {
354+
sendEmailOptions = options;
355+
},
356+
sendPasswordResetEmail: () => Promise.resolve(),
357+
sendMail: () => {},
358+
};
359+
const verifyUserEmails = {
360+
method(req) {
361+
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']);
362+
if (req.object.get('username') === 'no_email') {
363+
return false;
364+
}
365+
return true;
366+
},
367+
};
368+
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
369+
await reconfigureServer({
370+
appName: 'emailVerifyToken',
371+
verifyUserEmails: verifyUserEmails.method,
372+
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
373+
emailAdapter: emailAdapter,
374+
emailVerifyTokenValidityDuration: 5, // 5 seconds
375+
publicServerURL: 'http://localhost:8378/1',
376+
});
377+
const user = new Parse.User();
378+
user.setUsername('no_email');
379+
user.setPassword('expiringToken');
380+
user.set('email', '[email protected]');
381+
await user.signUp();
382+
expect(sendEmailOptions).toBeUndefined();
383+
expect(user.getSessionToken()).toBeDefined();
384+
expect(verifySpy).toHaveBeenCalledTimes(2);
385+
const user2 = new Parse.User();
386+
user2.setUsername('email');
387+
user2.setPassword('expiringToken');
388+
user2.set('email', '[email protected]');
389+
await user2.signUp();
390+
expect(user2.getSessionToken()).toBeUndefined();
391+
expect(sendEmailOptions).toBeDefined();
392+
expect(verifySpy).toHaveBeenCalledTimes(4);
393+
});
394+
395+
it('can conditionally send user email verification', async () => {
396+
const emailAdapter = {
397+
sendVerificationEmail: () => {},
398+
sendPasswordResetEmail: () => Promise.resolve(),
399+
sendMail: () => {},
400+
};
401+
const sendVerificationEmail = {
402+
method(req) {
403+
expect(req.user).toBeDefined();
404+
expect(req.master).toBeDefined();
405+
return false;
406+
},
407+
};
408+
const sendSpy = spyOn(sendVerificationEmail, 'method').and.callThrough();
409+
await reconfigureServer({
410+
appName: 'emailVerifyToken',
411+
verifyUserEmails: true,
412+
emailAdapter: emailAdapter,
413+
emailVerifyTokenValidityDuration: 5, // 5 seconds
414+
publicServerURL: 'http://localhost:8378/1',
415+
sendUserEmailVerification: sendVerificationEmail.method,
416+
});
417+
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
418+
const newUser = new Parse.User();
419+
newUser.setUsername('unsets_email_verify_token_expires_at');
420+
newUser.setPassword('expiringToken');
421+
newUser.set('email', '[email protected]');
422+
await newUser.signUp();
423+
await Parse.User.requestEmailVerification('[email protected]');
424+
expect(sendSpy).toHaveBeenCalledTimes(2);
425+
expect(emailSpy).toHaveBeenCalledTimes(0);
426+
});
427+
428+
it('beforeSave options do not change existing behaviour', async () => {
429+
let sendEmailOptions;
430+
const emailAdapter = {
431+
sendVerificationEmail: options => {
432+
sendEmailOptions = options;
433+
},
434+
sendPasswordResetEmail: () => Promise.resolve(),
435+
sendMail: () => {},
436+
};
437+
await reconfigureServer({
438+
appName: 'emailVerifyToken',
439+
verifyUserEmails: true,
440+
emailAdapter: emailAdapter,
441+
emailVerifyTokenValidityDuration: 5, // 5 seconds
442+
publicServerURL: 'http://localhost:8378/1',
443+
});
444+
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
445+
const newUser = new Parse.User();
446+
newUser.setUsername('unsets_email_verify_token_expires_at');
447+
newUser.setPassword('expiringToken');
448+
newUser.set('email', '[email protected]');
449+
await newUser.signUp();
450+
const response = await request({
451+
url: sendEmailOptions.link,
452+
followRedirects: false,
453+
});
454+
expect(response.status).toEqual(302);
455+
const config = Config.get('test');
456+
const results = await config.database.find('_User', {
457+
username: 'unsets_email_verify_token_expires_at',
458+
});
459+
460+
expect(results.length).toBe(1);
461+
const user = results[0];
462+
expect(typeof user).toBe('object');
463+
expect(user.emailVerified).toEqual(true);
464+
expect(typeof user._email_verify_token).toBe('undefined');
465+
expect(typeof user._email_verify_token_expires_at).toBe('undefined');
466+
expect(emailSpy).toHaveBeenCalled();
467+
});
468+
291469
it('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => {
292470
const user = new Parse.User();
293471
let sendEmailOptions;

spec/UserController.spec.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
const UserController = require('../lib/Controllers/UserController').UserController;
21
const emailAdapter = require('./support/MockEmailAdapter');
32

43
describe('UserController', () => {
@@ -11,11 +10,14 @@ describe('UserController', () => {
1110
describe('sendVerificationEmail', () => {
1211
describe('parseFrameURL not provided', () => {
1312
it('uses publicServerURL', async done => {
14-
await reconfigureServer({
13+
const server = await reconfigureServer({
1514
publicServerURL: 'http://www.example.com',
1615
customPages: {
1716
parseFrameURL: undefined,
1817
},
18+
verifyUserEmails: true,
19+
emailAdapter,
20+
appName: 'test',
1921
});
2022
emailAdapter.sendVerificationEmail = options => {
2123
expect(options.link).toEqual(
@@ -24,20 +26,20 @@ describe('UserController', () => {
2426
emailAdapter.sendVerificationEmail = () => Promise.resolve();
2527
done();
2628
};
27-
const userController = new UserController(emailAdapter, 'test', {
28-
verifyUserEmails: true,
29-
});
30-
userController.sendVerificationEmail(user);
29+
server.config.userController.sendVerificationEmail(user);
3130
});
3231
});
3332

3433
describe('parseFrameURL provided', () => {
3534
it('uses parseFrameURL and includes the destination in the link parameter', async done => {
36-
await reconfigureServer({
35+
const server = await reconfigureServer({
3736
publicServerURL: 'http://www.example.com',
3837
customPages: {
3938
parseFrameURL: 'http://someother.example.com/handle-parse-iframe',
4039
},
40+
verifyUserEmails: true,
41+
emailAdapter,
42+
appName: 'test',
4143
});
4244
emailAdapter.sendVerificationEmail = options => {
4345
expect(options.link).toEqual(
@@ -46,10 +48,7 @@ describe('UserController', () => {
4648
emailAdapter.sendVerificationEmail = () => Promise.resolve();
4749
done();
4850
};
49-
const userController = new UserController(emailAdapter, 'test', {
50-
verifyUserEmails: true,
51-
});
52-
userController.sendVerificationEmail(user);
51+
server.config.userController.sendVerificationEmail(user);
5352
});
5453
});
5554
});

src/Adapters/Files/GridFSBucketAdapter.js

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ export class GridFSBucketAdapter extends FilesAdapter {
2828
this._algorithm = 'aes-256-gcm';
2929
this._encryptionKey =
3030
encryptionKey !== undefined
31-
? crypto.createHash('sha256').update(String(encryptionKey)).digest('base64').substr(0, 32)
31+
? crypto
32+
.createHash('sha256')
33+
.update(String(encryptionKey))
34+
.digest('base64')
35+
.substring(0, 32)
3236
: null;
3337
const defaultMongoOptions = {
3438
useNewUrlParser: true,
@@ -138,8 +142,8 @@ export class GridFSBucketAdapter extends FilesAdapter {
138142
}
139143

140144
async rotateEncryptionKey(options = {}) {
141-
var fileNames = [];
142-
var oldKeyFileAdapter = {};
145+
let fileNames = [];
146+
let oldKeyFileAdapter = {};
143147
const bucket = await this._getBucket();
144148
if (options.oldKey !== undefined) {
145149
oldKeyFileAdapter = new GridFSBucketAdapter(
@@ -158,51 +162,22 @@ export class GridFSBucketAdapter extends FilesAdapter {
158162
fileNames.push(file.filename);
159163
});
160164
}
161-
return new Promise(resolve => {
162-
var fileNamesNotRotated = fileNames;
163-
var fileNamesRotated = [];
164-
var fileNameTotal = fileNames.length;
165-
var fileNameIndex = 0;
166-
fileNames.forEach(fileName => {
167-
oldKeyFileAdapter
168-
.getFileData(fileName)
169-
.then(plainTextData => {
170-
//Overwrite file with data encrypted with new key
171-
this.createFile(fileName, plainTextData)
172-
.then(() => {
173-
fileNamesRotated.push(fileName);
174-
fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
175-
return value !== fileName;
176-
});
177-
fileNameIndex += 1;
178-
if (fileNameIndex == fileNameTotal) {
179-
resolve({
180-
rotated: fileNamesRotated,
181-
notRotated: fileNamesNotRotated,
182-
});
183-
}
184-
})
185-
.catch(() => {
186-
fileNameIndex += 1;
187-
if (fileNameIndex == fileNameTotal) {
188-
resolve({
189-
rotated: fileNamesRotated,
190-
notRotated: fileNamesNotRotated,
191-
});
192-
}
193-
});
194-
})
195-
.catch(() => {
196-
fileNameIndex += 1;
197-
if (fileNameIndex == fileNameTotal) {
198-
resolve({
199-
rotated: fileNamesRotated,
200-
notRotated: fileNamesNotRotated,
201-
});
202-
}
203-
});
204-
});
205-
});
165+
let fileNamesNotRotated = fileNames;
166+
const fileNamesRotated = [];
167+
for (const fileName of fileNames) {
168+
try {
169+
const plainTextData = await oldKeyFileAdapter.getFileData(fileName);
170+
// Overwrite file with data encrypted with new key
171+
await this.createFile(fileName, plainTextData);
172+
fileNamesRotated.push(fileName);
173+
fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
174+
return value !== fileName;
175+
});
176+
} catch (err) {
177+
continue;
178+
}
179+
}
180+
return { rotated: fileNamesRotated, notRotated: fileNamesNotRotated };
206181
}
207182

208183
getFileLocation(config, filename) {

0 commit comments

Comments
 (0)