refactor: Add option to convert Parse.Object to instance in Cloud Function payload (#8656)

This commit is contained in:
Manuel
2023-06-23 16:29:32 +02:00
committed by GitHub
parent 4ad0800508
commit e212eb5195
24 changed files with 467 additions and 158 deletions

View File

@@ -13,6 +13,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | deprecated | - |
| DEPPS10 | Config option `encodeParseObjectInCloudFunction` defaults to `true` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 7.0.0 (2024) | deprecated | - |
[i_deprecation]: ## "The version and date of the deprecation."
[i_removal]: ## "The version and date of the planned removal."

View File

@@ -1,3 +1,35 @@
# [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)
### Features
* 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))
# [6.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.3.0-alpha.1) (2023-06-18)
### Bug Fixes
* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
### Features
* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
### Reverts
* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
# [6.1.0-alpha.20](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.19...6.1.0-alpha.20) (2023-06-09)

View File

@@ -29,7 +29,7 @@
"template": "./node_modules/clean-jsdoc-theme",
"theme_opts": {
"default_theme": "dark",
"title": "Parse Server",
"title": "<img src='../.github/parse-server-logo.png' class='logo'/>",
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
}
},

23
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "parse-server",
"version": "6.3.0-beta.1",
"version": "6.3.0-alpha.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
"version": "6.3.0-beta.1",
"version": "6.3.0-alpha.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -46,7 +46,7 @@
"pluralize": "8.0.0",
"rate-limit-redis": "3.0.2",
"redis": "4.6.6",
"semver": "^7.5.1",
"semver": "^7.5.2",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
"uuid": "9.0.0",
@@ -15213,6 +15213,11 @@
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
},
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": {
"node": ">=0.10.0"
}
@@ -18173,9 +18178,9 @@
}
},
"node_modules/semver": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -34401,9 +34406,9 @@
}
},
"semver": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"requires": {
"lru-cache": "^6.0.0"
},

View File

@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "6.3.0-beta.1",
"version": "6.3.0-alpha.2",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -55,7 +55,7 @@
"pluralize": "8.0.0",
"rate-limit-redis": "3.0.2",
"redis": "4.6.6",
"semver": "7.5.1",
"semver": "7.5.2",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
"uuid": "9.0.0",

View File

@@ -1,6 +1,6 @@
const pkg = require('./package.json');
const version = parseFloat(process.version.substr(1));
const version = parseFloat(process.version.substring(1));
const minimum = parseFloat(pkg.engines.node.match(/\d+/g).join('.'));
module.exports = function () {

View File

@@ -27,3 +27,8 @@ npm run docs
mkdir -p "docs/api/${DEST}"
cp -R out/* "docs/api/${DEST}"
# Copy other resources
RESOURCE_DIR=".github"
mkdir -p "docs/${RESOURCE_DIR}"
cp "./.github/parse-server-logo.png" "docs/${RESOURCE_DIR}/"

View File

@@ -255,7 +255,16 @@ function inject(t, list) {
props.push(t.objectProperty(t.stringLiteral('action'), action));
}
if (elt.defaultValue) {
const parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
if (!parsedValue) {
for (const type of elt.typeAnnotation.types) {
elt.type = type.type;
parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
if (parsedValue) {
break;
}
}
}
if (parsedValue) {
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
} else {

View File

@@ -1353,7 +1353,27 @@ describe('Cloud Code', () => {
});
});
it('should not encode Parse Objects', async () => {
const user = new Parse.User();
user.setUsername('username');
user.setPassword('password');
user.set('deleted', false);
await user.signUp();
Parse.Cloud.define(
'deleteAccount',
async req => {
expect(req.params.object instanceof Parse.Object).not.toBeTrue();
return 'Object deleted';
},
{
requireMaster: true,
}
);
await Parse.Cloud.run('deleteAccount', { object: user.toPointer() }, { useMasterKey: true });
});
it('allow cloud to encode Parse Objects', async () => {
await reconfigureServer({ encodeParseObjectInCloudFunction: true });
const user = new Parse.User();
user.setUsername('username');
user.setPassword('password');

View File

@@ -288,6 +288,184 @@ describe('Email Verification Token Expiration: ', () => {
});
});
it('can conditionally send emails', async () => {
let sendEmailOptions;
const emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {},
};
const verifyUserEmails = {
method(req) {
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']);
return false;
},
};
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: verifyUserEmails.method,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1',
});
const beforeSave = {
method(req) {
req.object.set('emailVerified', true);
},
};
const saveSpy = spyOn(beforeSave, 'method').and.callThrough();
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
Parse.Cloud.beforeSave(Parse.User, beforeSave.method);
const user = new Parse.User();
user.setUsername('sets_email_verify_token_expires_at');
user.setPassword('expiringToken');
user.set('email', 'user@example.com');
await user.signUp();
const config = Config.get('test');
const results = await config.database.find(
'_User',
{
username: 'sets_email_verify_token_expires_at',
},
{},
Auth.maintenance(config)
);
expect(results.length).toBe(1);
const user_data = results[0];
expect(typeof user_data).toBe('object');
expect(user_data.emailVerified).toEqual(true);
expect(user_data._email_verify_token).toBeUndefined();
expect(user_data._email_verify_token_expires_at).toBeUndefined();
expect(emailSpy).not.toHaveBeenCalled();
expect(saveSpy).toHaveBeenCalled();
expect(sendEmailOptions).toBeUndefined();
expect(verifySpy).toHaveBeenCalled();
});
it('can conditionally send emails and allow conditional login', async () => {
let sendEmailOptions;
const emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {},
};
const verifyUserEmails = {
method(req) {
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']);
if (req.object.get('username') === 'no_email') {
return false;
}
return true;
},
};
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1',
});
const user = new Parse.User();
user.setUsername('no_email');
user.setPassword('expiringToken');
user.set('email', 'user@example.com');
await user.signUp();
expect(sendEmailOptions).toBeUndefined();
expect(user.getSessionToken()).toBeDefined();
expect(verifySpy).toHaveBeenCalledTimes(2);
const user2 = new Parse.User();
user2.setUsername('email');
user2.setPassword('expiringToken');
user2.set('email', 'user2@example.com');
await user2.signUp();
expect(user2.getSessionToken()).toBeUndefined();
expect(sendEmailOptions).toBeDefined();
expect(verifySpy).toHaveBeenCalledTimes(4);
});
it('can conditionally send user email verification', async () => {
const emailAdapter = {
sendVerificationEmail: () => {},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {},
};
const sendVerificationEmail = {
method(req) {
expect(req.user).toBeDefined();
expect(req.master).toBeDefined();
return false;
},
};
const sendSpy = spyOn(sendVerificationEmail, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1',
sendUserEmailVerification: sendVerificationEmail.method,
});
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
const newUser = new Parse.User();
newUser.setUsername('unsets_email_verify_token_expires_at');
newUser.setPassword('expiringToken');
newUser.set('email', 'user@example.com');
await newUser.signUp();
await Parse.User.requestEmailVerification('user@example.com');
expect(sendSpy).toHaveBeenCalledTimes(2);
expect(emailSpy).toHaveBeenCalledTimes(0);
});
it('beforeSave options do not change existing behaviour', async () => {
let sendEmailOptions;
const emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {},
};
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1',
});
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
const newUser = new Parse.User();
newUser.setUsername('unsets_email_verify_token_expires_at');
newUser.setPassword('expiringToken');
newUser.set('email', 'user@parse.com');
await newUser.signUp();
const response = await request({
url: sendEmailOptions.link,
followRedirects: false,
});
expect(response.status).toEqual(302);
const config = Config.get('test');
const results = await config.database.find('_User', {
username: 'unsets_email_verify_token_expires_at',
});
expect(results.length).toBe(1);
const user = results[0];
expect(typeof user).toBe('object');
expect(user.emailVerified).toEqual(true);
expect(typeof user._email_verify_token).toBe('undefined');
expect(typeof user._email_verify_token_expires_at).toBe('undefined');
expect(emailSpy).toHaveBeenCalled();
});
it('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => {
const user = new Parse.User();
let sendEmailOptions;

View File

@@ -1,4 +1,3 @@
const UserController = require('../lib/Controllers/UserController').UserController;
const emailAdapter = require('./support/MockEmailAdapter');
describe('UserController', () => {
@@ -11,11 +10,14 @@ describe('UserController', () => {
describe('sendVerificationEmail', () => {
describe('parseFrameURL not provided', () => {
it('uses publicServerURL', async done => {
await reconfigureServer({
const server = await reconfigureServer({
publicServerURL: 'http://www.example.com',
customPages: {
parseFrameURL: undefined,
},
verifyUserEmails: true,
emailAdapter,
appName: 'test',
});
emailAdapter.sendVerificationEmail = options => {
expect(options.link).toEqual(
@@ -24,20 +26,20 @@ describe('UserController', () => {
emailAdapter.sendVerificationEmail = () => Promise.resolve();
done();
};
const userController = new UserController(emailAdapter, 'test', {
verifyUserEmails: true,
});
userController.sendVerificationEmail(user);
server.config.userController.sendVerificationEmail(user);
});
});
describe('parseFrameURL provided', () => {
it('uses parseFrameURL and includes the destination in the link parameter', async done => {
await reconfigureServer({
const server = await reconfigureServer({
publicServerURL: 'http://www.example.com',
customPages: {
parseFrameURL: 'http://someother.example.com/handle-parse-iframe',
},
verifyUserEmails: true,
emailAdapter,
appName: 'test',
});
emailAdapter.sendVerificationEmail = options => {
expect(options.link).toEqual(
@@ -46,10 +48,7 @@ describe('UserController', () => {
emailAdapter.sendVerificationEmail = () => Promise.resolve();
done();
};
const userController = new UserController(emailAdapter, 'test', {
verifyUserEmails: true,
});
userController.sendVerificationEmail(user);
server.config.userController.sendVerificationEmail(user);
});
});
});

View File

@@ -28,7 +28,11 @@ export class GridFSBucketAdapter extends FilesAdapter {
this._algorithm = 'aes-256-gcm';
this._encryptionKey =
encryptionKey !== undefined
? crypto.createHash('sha256').update(String(encryptionKey)).digest('base64').substr(0, 32)
? crypto
.createHash('sha256')
.update(String(encryptionKey))
.digest('base64')
.substring(0, 32)
: null;
const defaultMongoOptions = {
useNewUrlParser: true,
@@ -138,8 +142,8 @@ export class GridFSBucketAdapter extends FilesAdapter {
}
async rotateEncryptionKey(options = {}) {
var fileNames = [];
var oldKeyFileAdapter = {};
let fileNames = [];
let oldKeyFileAdapter = {};
const bucket = await this._getBucket();
if (options.oldKey !== undefined) {
oldKeyFileAdapter = new GridFSBucketAdapter(
@@ -158,51 +162,22 @@ export class GridFSBucketAdapter extends FilesAdapter {
fileNames.push(file.filename);
});
}
return new Promise(resolve => {
var fileNamesNotRotated = fileNames;
var fileNamesRotated = [];
var fileNameTotal = fileNames.length;
var fileNameIndex = 0;
fileNames.forEach(fileName => {
oldKeyFileAdapter
.getFileData(fileName)
.then(plainTextData => {
//Overwrite file with data encrypted with new key
this.createFile(fileName, plainTextData)
.then(() => {
fileNamesRotated.push(fileName);
fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
return value !== fileName;
});
fileNameIndex += 1;
if (fileNameIndex == fileNameTotal) {
resolve({
rotated: fileNamesRotated,
notRotated: fileNamesNotRotated,
});
}
})
.catch(() => {
fileNameIndex += 1;
if (fileNameIndex == fileNameTotal) {
resolve({
rotated: fileNamesRotated,
notRotated: fileNamesNotRotated,
});
}
});
})
.catch(() => {
fileNameIndex += 1;
if (fileNameIndex == fileNameTotal) {
resolve({
rotated: fileNamesRotated,
notRotated: fileNamesNotRotated,
});
}
});
});
});
let fileNamesNotRotated = fileNames;
const fileNamesRotated = [];
for (const fileName of fileNames) {
try {
const plainTextData = await oldKeyFileAdapter.getFileData(fileName);
// Overwrite file with data encrypted with new key
await this.createFile(fileName, plainTextData);
fileNamesRotated.push(fileName);
fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
return value !== fileName;
});
} catch (err) {
continue;
}
}
return { rotated: fileNamesRotated, notRotated: fileNamesNotRotated };
}
getFileLocation(config, filename) {

View File

@@ -231,7 +231,7 @@ const transformAggregateField = fieldName => {
if (fieldName === '$_updated_at') {
return 'updatedAt';
}
return fieldName.substr(1);
return fieldName.substring(1);
};
const validateKeys = object => {
@@ -1921,14 +1921,14 @@ export class PostgresStorageAdapter implements StorageAdapter {
};
}
if (object[fieldName] && schema.fields[fieldName].type === 'Polygon') {
let coords = object[fieldName];
coords = coords.substr(2, coords.length - 4).split('),(');
coords = coords.map(point => {
let coords = new String(object[fieldName]);
coords = coords.substring(2, coords.length - 2).split('),(');
const updatedCoords = coords.map(point => {
return [parseFloat(point.split(',')[1]), parseFloat(point.split(',')[0])];
});
object[fieldName] = {
__type: 'Polygon',
coordinates: coords,
coordinates: updatedCoords,
};
}
if (object[fieldName] && schema.fields[fieldName].type === 'File') {
@@ -2634,7 +2634,7 @@ function literalizeRegexPart(s: string) {
const result1: any = s.match(matcher1);
if (result1 && result1.length > 1 && result1.index > -1) {
// process regex that has a beginning and an end specified for the literal text
const prefix = s.substr(0, result1.index);
const prefix = s.substring(0, result1.index);
const remaining = result1[1];
return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
@@ -2644,7 +2644,7 @@ function literalizeRegexPart(s: string) {
const matcher2 = /\\Q((?!\\E).*)$/;
const result2: any = s.match(matcher2);
if (result2 && result2.length > 1 && result2.index > -1) {
const prefix = s.substr(0, result2.index);
const prefix = s.substring(0, result2.index);
const remaining = result2[1];
return literalizeRegexPart(prefix) + createLiteralRegex(remaining);

View File

@@ -25,7 +25,7 @@ function removeTrailingSlash(str) {
return str;
}
if (str.endsWith('/')) {
str = str.substr(0, str.length - 1);
str = str.substring(0, str.length - 1);
}
return str;
}

View File

@@ -32,20 +32,33 @@ export class UserController extends AdaptableController {
}
get shouldVerifyEmails() {
return this.options.verifyUserEmails;
return (this.config || this.options).verifyUserEmails;
}
setEmailVerifyToken(user) {
if (this.shouldVerifyEmails) {
user._email_verify_token = randomString(25);
user.emailVerified = false;
if (this.config.emailVerifyTokenValidityDuration) {
user._email_verify_token_expires_at = Parse._encode(
this.config.generateEmailVerifyTokenExpiresAt()
);
}
async setEmailVerifyToken(user, req, storage = {}) {
let shouldSendEmail = this.shouldVerifyEmails;
if (typeof shouldSendEmail === 'function') {
const response = await Promise.resolve(shouldSendEmail(req));
shouldSendEmail = response !== false;
}
if (!shouldSendEmail) {
return false;
}
storage.sendVerificationEmail = true;
user._email_verify_token = randomString(25);
if (
!storage.fieldsChangedByTrigger ||
!storage.fieldsChangedByTrigger.includes('emailVerified')
) {
user.emailVerified = false;
}
if (this.config.emailVerifyTokenValidityDuration) {
user._email_verify_token_expires_at = Parse._encode(
this.config.generateEmailVerifyTokenExpiresAt()
);
}
return true;
}
verifyEmail(username, token) {
@@ -131,27 +144,39 @@ export class UserController extends AdaptableController {
});
}
sendVerificationEmail(user) {
async sendVerificationEmail(user, req) {
if (!this.shouldVerifyEmails) {
return;
}
const token = encodeURIComponent(user._email_verify_token);
// We may need to fetch the user in case of update email
this.getUserIfNeeded(user).then(user => {
const username = encodeURIComponent(user.username);
const fetchedUser = await this.getUserIfNeeded(user);
let shouldSendEmail = this.config.sendUserEmailVerification;
if (typeof shouldSendEmail === 'function') {
const response = await Promise.resolve(
this.config.sendUserEmailVerification({
user: Parse.Object.fromJSON({ className: '_User', ...fetchedUser }),
master: req.auth?.isMaster,
})
);
shouldSendEmail = !!response;
}
if (!shouldSendEmail) {
return;
}
const username = encodeURIComponent(user.username);
const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
const options = {
appName: this.config.appName,
link: link,
user: inflate('_User', user),
};
if (this.adapter.sendVerificationEmail) {
this.adapter.sendVerificationEmail(options);
} else {
this.adapter.sendMail(this.defaultVerificationEmail(options));
}
});
const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
const options = {
appName: this.config.appName,
link: link,
user: inflate('_User', fetchedUser),
};
if (this.adapter.sendVerificationEmail) {
this.adapter.sendVerificationEmail(options);
} else {
this.adapter.sendMail(this.defaultVerificationEmail(options));
}
}
/**
@@ -160,7 +185,7 @@ export class UserController extends AdaptableController {
* @param user
* @returns {*}
*/
regenerateEmailVerifyToken(user) {
async regenerateEmailVerifyToken(user, master) {
const { _email_verify_token } = user;
let { _email_verify_token_expires_at } = user;
if (_email_verify_token_expires_at && _email_verify_token_expires_at.__type === 'Date') {
@@ -174,19 +199,22 @@ export class UserController extends AdaptableController {
) {
return Promise.resolve();
}
this.setEmailVerifyToken(user);
const shouldSend = await this.setEmailVerifyToken(user, { user, master });
if (!shouldSend) {
return;
}
return this.config.database.update('_User', { username: user.username }, user);
}
resendVerificationEmail(username) {
return this.getUserIfNeeded({ username: username }).then(aUser => {
if (!aUser || aUser.emailVerified) {
throw undefined;
}
return this.regenerateEmailVerifyToken(aUser).then(() => {
this.sendVerificationEmail(aUser);
});
});
async resendVerificationEmail(username, req) {
const aUser = await this.getUserIfNeeded({ username: username });
if (!aUser || aUser.emailVerified) {
throw undefined;
}
const generate = await this.regenerateEmailVerifyToken(aUser, req.auth?.isMaster);
if (generate) {
this.sendVerificationEmail(aUser, req);
}
}
setPasswordResetToken(email) {

View File

@@ -18,4 +18,5 @@
module.exports = [
{ optionKey: 'allowClientClassCreation', changeNewDefault: 'false' },
{ optionKey: 'allowExpiredAuthDataToken', changeNewDefault: 'false' },
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
];

View File

@@ -210,6 +210,13 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser,
default: false,
},
encodeParseObjectInCloudFunction: {
env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
help:
'If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br>\u2139\uFE0F The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.',
action: parsers.booleanParser,
default: false,
},
encryptionKey: {
env: 'PARSE_SERVER_ENCRYPTION_KEY',
help: 'Key for encrypting your files',
@@ -496,6 +503,12 @@ module.exports.ParseServerOptions = {
action: parsers.objectParser,
default: {},
},
sendUserEmailVerification: {
env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION',
help:
'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.<br><br>Default is `true`.<br>',
default: true,
},
serverCloseComplete: {
env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE',
help: 'Callback when server has closed',
@@ -542,8 +555,7 @@ module.exports.ParseServerOptions = {
verifyUserEmails: {
env: 'PARSE_SERVER_VERIFY_USER_EMAILS',
help:
'Set to `true` to require users to verify their email address to complete the sign-up process.<br><br>Default is `false`.',
action: parsers.booleanParser,
'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.<br><br>Default is `false`.',
default: false,
},
webhookKey: {

View File

@@ -40,6 +40,7 @@
* @property {Number} emailVerifyTokenValidityDuration Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.<br><br>For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).<br><br>Default is `undefined`.<br>Requires option `verifyUserEmails: true`.
* @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
* @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br> The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
* @property {String} encryptionKey Key for encrypting your files
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
* @property {Boolean} expireInactiveSessions Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.
@@ -89,6 +90,7 @@
* @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false.
* @property {SchemaOptions} schema Defined schema
* @property {SecurityOptions} security The security options to identify and report weak security settings.
* @property {Boolean} sendUserEmailVerification Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.<br><br>Default is `true`.<br>
* @property {Function} serverCloseComplete Callback when server has closed
* @property {String} serverURL URL to your parse server with http:// or https://.
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
@@ -97,7 +99,7 @@
* @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
* @property {Boolean} verbose Set the logging to verbose
* @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process.<br><br>Default is `false`.
* @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.<br><br>Default is `false`.
* @property {String} webhookKey Key sent with outgoing webhook calls
*/

View File

@@ -153,11 +153,11 @@ export interface ParseServerOptions {
/* Max file size for uploads, defaults to 20mb
:DEFAULT: 20mb */
maxUploadSize: ?string;
/* Set to `true` to require users to verify their email address to complete the sign-up process.
/* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.
<br><br>
Default is `false`.
:DEFAULT: false */
verifyUserEmails: ?boolean;
verifyUserEmails: ?(boolean | void);
/* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.
<br><br>
Default is `false`.
@@ -188,6 +188,12 @@ export interface ParseServerOptions {
Requires option `verifyUserEmails: true`.
:DEFAULT: false */
emailVerifyTokenReuseIfValid: ?boolean;
/* Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.
<br><br>
Default is `true`.
<br>
:DEFAULT: true */
sendUserEmailVerification: ?(boolean | void);
/* The account lockout policy for failed login attempts. */
accountLockout: ?AccountLockoutOptions;
/* The password policy for enforcing password related rules. */
@@ -196,6 +202,9 @@ export interface ParseServerOptions {
cacheAdapter: ?Adapter<CacheAdapter>;
/* Adapter module for email sending */
emailAdapter: ?Adapter<MailAdapter>;
/* If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br> The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
:DEFAULT: false */
encodeParseObjectInCloudFunction: ?boolean;
/* Public URL to your parse server with http:// or https://.
:ENV: PARSE_PUBLIC_SERVER_URL */
publicServerURL: ?string;

View File

@@ -113,6 +113,9 @@ RestWrite.prototype.execute = function () {
.then(() => {
return this.validateAuthData();
})
.then(() => {
return this.checkRestrictedFields();
})
.then(() => {
return this.runBeforeSaveTrigger();
})
@@ -603,17 +606,23 @@ RestWrite.prototype.handleAuthData = async function (authData) {
}
};
// The non-third-party parts of User transformation
RestWrite.prototype.transformUser = function () {
var promise = Promise.resolve();
RestWrite.prototype.checkRestrictedFields = async function () {
if (this.className !== '_User') {
return promise;
return;
}
if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) {
const error = `Clients aren't allowed to manually update email verification.`;
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
}
};
// The non-third-party parts of User transformation
RestWrite.prototype.transformUser = function () {
var promise = Promise.resolve();
if (this.className !== '_User') {
return promise;
}
// Do not cleanup session if objectId is not set
if (this.query && this.objectId()) {
@@ -751,8 +760,14 @@ RestWrite.prototype._validateEmail = function () {
Object.keys(this.data.authData)[0] === 'anonymous')
) {
// We updated the email, send a new validation
this.storage['sendVerificationEmail'] = true;
this.config.userController.setEmailVerifyToken(this.data);
const { originalObject, updatedObject } = this.buildParseObjects();
const request = {
original: originalObject,
object: updatedObject,
master: this.auth.isMaster,
ip: this.config.ip,
};
return this.config.userController.setEmailVerifyToken(this.data, request, this.storage);
}
});
};
@@ -864,7 +879,7 @@ RestWrite.prototype._validatePasswordHistory = function () {
return Promise.resolve();
};
RestWrite.prototype.createSessionTokenIfNeeded = function () {
RestWrite.prototype.createSessionTokenIfNeeded = async function () {
if (this.className !== '_User') {
return;
}
@@ -878,13 +893,31 @@ RestWrite.prototype.createSessionTokenIfNeeded = function () {
}
if (
!this.storage.authProvider && // signup call, with
this.config.preventLoginWithUnverifiedEmail && // no login without verification
this.config.preventLoginWithUnverifiedEmail === true && // no login without verification
this.config.verifyUserEmails
) {
// verification is on
this.storage.rejectSignup = true;
return;
}
if (!this.storage.authProvider && this.config.verifyUserEmails) {
let shouldPreventUnverifedLogin = this.config.preventLoginWithUnverifiedEmail;
if (typeof this.config.preventLoginWithUnverifiedEmail === 'function') {
const { originalObject, updatedObject } = this.buildParseObjects();
const request = {
original: originalObject,
object: updatedObject,
master: this.auth.isMaster,
ip: this.config.ip,
};
shouldPreventUnverifedLogin = await Promise.resolve(
this.config.preventLoginWithUnverifiedEmail(request)
);
}
if (shouldPreventUnverifedLogin === true) {
return;
}
}
return this.createSessionToken();
};
@@ -1010,7 +1043,7 @@ RestWrite.prototype.handleFollowup = function () {
if (this.storage && this.storage['sendVerificationEmail']) {
delete this.storage['sendVerificationEmail'];
// Fire and forget!
this.config.userController.sendVerificationEmail(this.data);
this.config.userController.sendVerificationEmail(this.data, { auth: this.auth });
return this.handleFollowup.bind(this);
}
};

View File

@@ -9,7 +9,7 @@ import { jobStatusHandler } from '../StatusHandler';
import _ from 'lodash';
import { logger } from '../logger';
function parseObject(obj) {
function parseObject(obj, config) {
if (Array.isArray(obj)) {
return obj.map(item => {
return parseObject(item);
@@ -18,21 +18,21 @@ function parseObject(obj) {
return Object.assign(new Date(obj.iso), obj);
} else if (obj && obj.__type == 'File') {
return Parse.File.fromJSON(obj);
} else if (obj && obj.__type == 'Pointer') {
} else if (obj && obj.__type == 'Pointer' && config.encodeParseObjectInCloudFunction) {
return Parse.Object.fromJSON({
__type: 'Pointer',
className: obj.className,
objectId: obj.objectId,
});
} else if (obj && typeof obj === 'object') {
return parseParams(obj);
return parseParams(obj, config);
} else {
return obj;
}
}
function parseParams(params) {
return _.mapValues(params, parseObject);
function parseParams(params, config) {
return _.mapValues(params, item => parseObject(item, config));
}
export class FunctionsRouter extends PromiseRouter {
@@ -66,7 +66,7 @@ export class FunctionsRouter extends PromiseRouter {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid job.');
}
let params = Object.assign({}, req.body, req.query);
params = parseParams(params);
params = parseParams(params, req.config);
const request = {
params: params,
log: req.config.loggerController,
@@ -126,7 +126,7 @@ export class FunctionsRouter extends PromiseRouter {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "${functionName}"`);
}
let params = Object.assign({}, req.body, req.query);
params = parseParams(params);
params = parseParams(params, req.config);
const request = {
params: params,
master: req.auth && req.auth.isMaster,

View File

@@ -125,7 +125,7 @@ export class PagesRouter extends PromiseRouter {
const userController = config.userController;
return userController.resendVerificationEmail(username).then(
return userController.resendVerificationEmail(username, req).then(
() => {
return this.goToPage(req, pages.emailVerificationSendSuccess);
},

View File

@@ -63,7 +63,7 @@ export class PublicAPIRouter extends PromiseRouter {
const userController = config.userController;
return userController.resendVerificationEmail(username).then(
return userController.resendVerificationEmail(username, req).then(
() => {
return Promise.resolve({
status: 302,

View File

@@ -447,7 +447,7 @@ export class UsersRouter extends ClassesRouter {
}
}
handleVerificationEmailRequest(req) {
async handleVerificationEmailRequest(req) {
this._throwOnBadEmailConfig(req);
const { email } = req.body;
@@ -461,25 +461,25 @@ export class UsersRouter extends ClassesRouter {
);
}
return req.config.database.find('_User', { email: email }).then(results => {
if (!results.length || results.length < 1) {
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`);
}
const user = results[0];
const results = await req.config.database.find('_User', { email: email });
if (!results.length || results.length < 1) {
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`);
}
const user = results[0];
// remove password field, messes with saving on postgres
delete user.password;
// remove password field, messes with saving on postgres
delete user.password;
if (user.emailVerified) {
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`);
}
if (user.emailVerified) {
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`);
}
const userController = req.config.userController;
return userController.regenerateEmailVerifyToken(user).then(() => {
userController.sendVerificationEmail(user);
return { response: {} };
});
});
const userController = req.config.userController;
const send = await userController.regenerateEmailVerifyToken(user, req.auth.isMaster);
if (send) {
userController.sendVerificationEmail(user, req);
}
return { response: {} };
}
async handleChallenge(req) {