mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ed9af48d1 | ||
|
|
94cee5bfaf | ||
|
|
50650a3626 | ||
|
|
c22cb0ae58 | ||
|
|
5e15403bc1 | ||
|
|
fcede163ca | ||
|
|
7b9fa18f96 |
@@ -1,3 +1,24 @@
|
||||
# [8.5.0-alpha.12](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.11...8.5.0-alpha.12) (2025-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `beforePasswordResetRequest` hook ([#9906](https://github.com/parse-community/parse-server/issues/9906)) ([94cee5b](https://github.com/parse-community/parse-server/commit/94cee5bfafca10c914c73cf17fcdb627a9f0837b))
|
||||
|
||||
# [8.5.0-alpha.11](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.10...8.5.0-alpha.11) (2025-11-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Deprecation warning logged at server launch for nested Parse Server option even if option is explicitly set ([#9934](https://github.com/parse-community/parse-server/issues/9934)) ([c22cb0a](https://github.com/parse-community/parse-server/commit/c22cb0ae58e64cd0e4597ab9610d57a1155c44a2))
|
||||
|
||||
# [8.5.0-alpha.10](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.9...8.5.0-alpha.10) (2025-11-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Queries with object field `authData.provider.id` are incorrectly transformed to `_auth_data_provider.id` for custom classes ([#9932](https://github.com/parse-community/parse-server/issues/9932)) ([7b9fa18](https://github.com/parse-community/parse-server/commit/7b9fa18f968ec084ea0b35dad2b5ba0451d59787))
|
||||
|
||||
# [8.5.0-alpha.9](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.8...8.5.0-alpha.9) (2025-11-17)
|
||||
|
||||
|
||||
|
||||
Generated
+14
-14
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"version": "8.5.0-alpha.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "parse-server",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"version": "8.5.0-alpha.12",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -3380,9 +3380,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
@@ -13313,9 +13313,9 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
@@ -25233,9 +25233,9 @@
|
||||
}
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
@@ -32207,9 +32207,9 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "8.5.0-alpha.9",
|
||||
"version": "8.5.0-alpha.12",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
|
||||
+159
-5
@@ -3307,19 +3307,19 @@ describe('afterFind hooks', () => {
|
||||
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
|
||||
expect(() => {
|
||||
Parse.Cloud.beforeLogin('SomeClass', () => { });
|
||||
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
|
||||
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
|
||||
expect(() => {
|
||||
Parse.Cloud.afterLogin(() => { });
|
||||
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
|
||||
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
|
||||
expect(() => {
|
||||
Parse.Cloud.afterLogin('_User', () => { });
|
||||
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
|
||||
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
|
||||
expect(() => {
|
||||
Parse.Cloud.afterLogin(Parse.User, () => { });
|
||||
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
|
||||
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
|
||||
expect(() => {
|
||||
Parse.Cloud.afterLogin('SomeClass', () => { });
|
||||
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
|
||||
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
|
||||
expect(() => {
|
||||
Parse.Cloud.afterLogout(() => { });
|
||||
}).not.toThrow();
|
||||
@@ -4656,3 +4656,157 @@ describe('sendEmail', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforePasswordResetRequest hook', () => {
|
||||
it('should run beforePasswordResetRequest with valid user', async () => {
|
||||
let hit = 0;
|
||||
let sendPasswordResetEmailCalled = false;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => {
|
||||
sendPasswordResetEmailCalled = true;
|
||||
},
|
||||
sendMail: () => {},
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
Parse.Cloud.beforePasswordResetRequest(req => {
|
||||
hit++;
|
||||
expect(req.object).toBeDefined();
|
||||
expect(req.object.get('email')).toEqual('test@example.com');
|
||||
expect(req.object.get('username')).toEqual('testuser');
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('testuser');
|
||||
user.setPassword('password');
|
||||
user.set('email', 'test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
await Parse.User.requestPasswordReset('test@example.com');
|
||||
expect(hit).toBe(1);
|
||||
expect(sendPasswordResetEmailCalled).toBe(true);
|
||||
});
|
||||
|
||||
it('should be able to block password reset request if an error is thrown', async () => {
|
||||
let hit = 0;
|
||||
let sendPasswordResetEmailCalled = false;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => {
|
||||
sendPasswordResetEmailCalled = true;
|
||||
},
|
||||
sendMail: () => {},
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
Parse.Cloud.beforePasswordResetRequest(req => {
|
||||
hit++;
|
||||
throw new Error('password reset blocked');
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('testuser');
|
||||
user.setPassword('password');
|
||||
user.set('email', 'test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
try {
|
||||
await Parse.User.requestPasswordReset('test@example.com');
|
||||
throw new Error('should not have sent password reset email.');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('password reset blocked');
|
||||
}
|
||||
expect(hit).toBe(1);
|
||||
expect(sendPasswordResetEmailCalled).toBe(false);
|
||||
});
|
||||
|
||||
it('should not run beforePasswordResetRequest if email does not exist', async () => {
|
||||
let hit = 0;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => {},
|
||||
sendMail: () => {},
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
Parse.Cloud.beforePasswordResetRequest(req => {
|
||||
hit++;
|
||||
});
|
||||
|
||||
await Parse.User.requestPasswordReset('nonexistent@example.com');
|
||||
|
||||
expect(hit).toBe(0);
|
||||
});
|
||||
|
||||
it('should have expected data in request in beforePasswordResetRequest', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => {},
|
||||
sendMail: () => {},
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
|
||||
const file = new Parse.File('myfile.txt', { base64 });
|
||||
await file.save();
|
||||
|
||||
Parse.Cloud.beforePasswordResetRequest(req => {
|
||||
expect(req.object).toBeDefined();
|
||||
expect(req.object.get('email')).toBeDefined();
|
||||
expect(req.object.get('email')).toBe('test2@example.com');
|
||||
expect(req.object.get('file')).toBeDefined();
|
||||
expect(req.object.get('file')).toBeInstanceOf(Parse.File);
|
||||
expect(req.object.get('file').name()).toContain('myfile.txt');
|
||||
expect(req.headers).toBeDefined();
|
||||
expect(req.ip).toBeDefined();
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.context).toBeDefined();
|
||||
expect(req.config).toBeDefined();
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('testuser2');
|
||||
user.setPassword('password');
|
||||
user.set('email', 'test2@example.com');
|
||||
user.set('file', file);
|
||||
await user.signUp();
|
||||
|
||||
await Parse.User.requestPasswordReset('test2@example.com');
|
||||
});
|
||||
|
||||
it('should validate that only _User class is allowed for beforePasswordResetRequest', () => {
|
||||
expect(() => {
|
||||
Parse.Cloud.beforePasswordResetRequest('SomeClass', () => { });
|
||||
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
|
||||
expect(() => {
|
||||
Parse.Cloud.beforePasswordResetRequest(() => { });
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
Parse.Cloud.beforePasswordResetRequest('_User', () => { });
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { });
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,4 +45,29 @@ describe('Deprecator', () => {
|
||||
`DeprecationWarning: ${options.usage} is deprecated and will be removed in a future version. ${options.solution}`
|
||||
);
|
||||
});
|
||||
|
||||
it('logs deprecation for nested option key with dot notation', async () => {
|
||||
deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }];
|
||||
|
||||
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
|
||||
await reconfigureServer();
|
||||
expect(logSpy.calls.all()[0].args[0]).toEqual(
|
||||
`DeprecationWarning: The Parse Server option '${deprecations[0].optionKey}' default will change to '${deprecations[0].changeNewDefault}' in a future version.`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not log deprecation for nested option key if option is set manually', async () => {
|
||||
deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }];
|
||||
|
||||
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
|
||||
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
|
||||
const Config = require('../lib/Config');
|
||||
const config = Config.get('test');
|
||||
// Directly test scanParseServerOptions with nested option set
|
||||
Deprecator.scanParseServerOptions({ databaseOptions: { allowPublicExplain: true } });
|
||||
expect(logSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -521,6 +521,23 @@ describe('parseObjectToMongoObjectForCreate', () => {
|
||||
expect(output.authData).toBe('random');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should only transform authData.provider.id for _User class', () => {
|
||||
// Test that for _User class, authData.facebook.id is transformed
|
||||
const userInput = {
|
||||
'authData.facebook.id': '10000000000000001',
|
||||
};
|
||||
const userOutput = transform.transformWhere('_User', userInput, { fields: {} });
|
||||
expect(userOutput['_auth_data_facebook.id']).toBe('10000000000000001');
|
||||
|
||||
// Test that for non-User classes, authData.facebook.id is NOT transformed
|
||||
const customInput = {
|
||||
'authData.facebook.id': '10000000000000001',
|
||||
};
|
||||
const customOutput = transform.transformWhere('SpamAlerts', customInput, { fields: {} });
|
||||
expect(customOutput['authData.facebook.id']).toBe('10000000000000001');
|
||||
expect(customOutput['_auth_data_facebook.id']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot have a custom field name beginning with underscore', done => {
|
||||
|
||||
@@ -122,4 +122,55 @@ describe('Utils', () => {
|
||||
expect(result).toBe('{"name":"test","number":42,"nested":{"key":"value"}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNestedProperty', () => {
|
||||
it('should get top-level property', () => {
|
||||
const obj = { foo: 'bar' };
|
||||
expect(Utils.getNestedProperty(obj, 'foo')).toBe('bar');
|
||||
});
|
||||
|
||||
it('should get nested property with dot notation', () => {
|
||||
const obj = { database: { options: { enabled: true } } };
|
||||
expect(Utils.getNestedProperty(obj, 'database.options.enabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent property', () => {
|
||||
const obj = { foo: 'bar' };
|
||||
expect(Utils.getNestedProperty(obj, 'baz')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent nested property', () => {
|
||||
const obj = { database: { options: {} } };
|
||||
expect(Utils.getNestedProperty(obj, 'database.options.enabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when path traverses non-object', () => {
|
||||
const obj = { database: 'string' };
|
||||
expect(Utils.getNestedProperty(obj, 'database.options.enabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for null object', () => {
|
||||
expect(Utils.getNestedProperty(null, 'foo')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for empty path', () => {
|
||||
const obj = { foo: 'bar' };
|
||||
expect(Utils.getNestedProperty(obj, '')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle value of 0', () => {
|
||||
const obj = { database: { timeout: 0 } };
|
||||
expect(Utils.getNestedProperty(obj, 'database.timeout')).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle value of false', () => {
|
||||
const obj = { database: { enabled: false } };
|
||||
expect(Utils.getNestedProperty(obj, 'database.enabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle value of empty string', () => {
|
||||
const obj = { database: { name: '' } };
|
||||
expect(Utils.getNestedProperty(obj, 'database.name')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -305,7 +305,7 @@ function transformQueryKeyValue(className, key, value, schema, count = false) {
|
||||
default: {
|
||||
// Other auth data
|
||||
const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
|
||||
if (authDataMatch) {
|
||||
if (authDataMatch && className === '_User') {
|
||||
const provider = authDataMatch[1];
|
||||
// Special-case auth data.
|
||||
return { key: `_auth_data_${provider}.id`, value };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logger from '../logger';
|
||||
import Deprecations from './Deprecations';
|
||||
import Utils from '../Utils';
|
||||
|
||||
/**
|
||||
* The deprecator class.
|
||||
@@ -21,7 +22,7 @@ class Deprecator {
|
||||
const changeNewDefault = deprecation.changeNewDefault;
|
||||
|
||||
// If default will change, only throw a warning if option is not set
|
||||
if (changeNewDefault != null && options[optionKey] == null) {
|
||||
if (changeNewDefault != null && Utils.getNestedProperty(options, optionKey) == null) {
|
||||
Deprecator._logOption({ optionKey, changeNewDefault, solution });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Types as TriggerTypes,
|
||||
getRequestObject,
|
||||
resolveError,
|
||||
inflate,
|
||||
} from '../triggers';
|
||||
import { promiseEnsureIdempotency } from '../middlewares';
|
||||
import RestWrite from '../RestWrite';
|
||||
@@ -444,21 +445,59 @@ export class UsersRouter extends ClassesRouter {
|
||||
if (!email && !token) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
|
||||
}
|
||||
|
||||
let userResults = null;
|
||||
let userData = null;
|
||||
|
||||
// We can find the user using token
|
||||
if (token) {
|
||||
const results = await req.config.database.find('_User', {
|
||||
userResults = await req.config.database.find('_User', {
|
||||
_perishable_token: token,
|
||||
_perishable_token_expires_at: { $lt: Parse._encode(new Date()) },
|
||||
});
|
||||
if (results && results[0] && results[0].email) {
|
||||
email = results[0].email;
|
||||
if (userResults?.length > 0) {
|
||||
userData = userResults[0];
|
||||
if (userData.email) {
|
||||
email = userData.email;
|
||||
}
|
||||
}
|
||||
// Or using email if no token provided
|
||||
} else if (typeof email === 'string') {
|
||||
userResults = await req.config.database.find(
|
||||
'_User',
|
||||
{ $or: [{ email }, { username: email, email: { $exists: false } }] },
|
||||
{ limit: 1 },
|
||||
Auth.maintenance(req.config)
|
||||
);
|
||||
if (userResults?.length > 0) {
|
||||
userData = userResults[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof email !== 'string') {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_EMAIL_ADDRESS,
|
||||
'you must provide a valid email string'
|
||||
);
|
||||
}
|
||||
|
||||
if (userData) {
|
||||
this._sanitizeAuthData(userData);
|
||||
// Get files attached to user
|
||||
await req.config.filesController.expandFilesInObject(req.config, userData);
|
||||
|
||||
const user = inflate('_User', userData);
|
||||
|
||||
await maybeRunTrigger(
|
||||
TriggerTypes.beforePasswordResetRequest,
|
||||
req.auth,
|
||||
user,
|
||||
null,
|
||||
req.config,
|
||||
req.info.context
|
||||
);
|
||||
}
|
||||
|
||||
const userController = req.config.userController;
|
||||
try {
|
||||
await userController.sendPasswordResetEmail(email);
|
||||
|
||||
@@ -444,6 +444,31 @@ class Utils {
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a nested property value from an object using dot notation.
|
||||
* @param {Object} obj The object to get the property from.
|
||||
* @param {String} path The property path in dot notation, e.g. 'databaseOptions.allowPublicExplain'.
|
||||
* @returns {any} The property value or undefined if not found.
|
||||
* @example
|
||||
* const obj = { database: { options: { enabled: true } } };
|
||||
* Utils.getNestedProperty(obj, 'database.options.enabled');
|
||||
* // Output: true
|
||||
*/
|
||||
static getNestedProperty(obj, path) {
|
||||
if (!obj || !path) {
|
||||
return undefined;
|
||||
}
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
for (const key of keys) {
|
||||
if (current == null || typeof current !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Utils;
|
||||
|
||||
@@ -349,6 +349,48 @@ ParseCloud.afterLogout = function (handler) {
|
||||
triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers the before password reset request function.
|
||||
*
|
||||
* **Available in Cloud Code only.**
|
||||
*
|
||||
* This function provides control in validating a password reset request
|
||||
* before the reset email is sent. It is triggered after the user is found
|
||||
* by email, but before the reset token is generated and the email is sent.
|
||||
*
|
||||
* Code example:
|
||||
*
|
||||
* ```
|
||||
* Parse.Cloud.beforePasswordResetRequest(request => {
|
||||
* if (request.object.get('banned')) {
|
||||
* throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User is banned.');
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @method beforePasswordResetRequest
|
||||
* @name Parse.Cloud.beforePasswordResetRequest
|
||||
* @param {Function} func The function to run before a password reset request. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest};
|
||||
*/
|
||||
ParseCloud.beforePasswordResetRequest = function (handler, validationHandler) {
|
||||
let className = '_User';
|
||||
if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
|
||||
// validation will occur downstream, this is to maintain internal
|
||||
// code consistency with the other hook types.
|
||||
className = triggers.getClassName(handler);
|
||||
handler = arguments[1];
|
||||
validationHandler = arguments.length >= 2 ? arguments[2] : null;
|
||||
}
|
||||
triggers.addTrigger(triggers.Types.beforePasswordResetRequest, className, handler, Parse.applicationId);
|
||||
if (validationHandler && validationHandler.rateLimit) {
|
||||
addRateLimit(
|
||||
{ requestPath: `/requestPasswordReset`, requestMethods: 'POST', ...validationHandler.rateLimit },
|
||||
Parse.applicationId,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers an after save function.
|
||||
*
|
||||
|
||||
+4
-2
@@ -6,6 +6,7 @@ export const Types = {
|
||||
beforeLogin: 'beforeLogin',
|
||||
afterLogin: 'afterLogin',
|
||||
afterLogout: 'afterLogout',
|
||||
beforePasswordResetRequest: 'beforePasswordResetRequest',
|
||||
beforeSave: 'beforeSave',
|
||||
afterSave: 'afterSave',
|
||||
beforeDelete: 'beforeDelete',
|
||||
@@ -58,10 +59,10 @@ function validateClassNameForTriggers(className, type) {
|
||||
// TODO: Allow proper documented way of using nested increment ops
|
||||
throw 'Only afterSave is allowed on _PushStatus';
|
||||
}
|
||||
if ((type === Types.beforeLogin || type === Types.afterLogin) && className !== '_User') {
|
||||
if ((type === Types.beforeLogin || type === Types.afterLogin || type === Types.beforePasswordResetRequest) && className !== '_User') {
|
||||
// TODO: check if upstream code will handle `Error` instance rather
|
||||
// than this anti-pattern of throwing strings
|
||||
throw 'Only the _User class is allowed for the beforeLogin and afterLogin triggers';
|
||||
throw 'Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers';
|
||||
}
|
||||
if (type === Types.afterLogout && className !== '_Session') {
|
||||
// TODO: check if upstream code will handle `Error` instance rather
|
||||
@@ -287,6 +288,7 @@ export function getRequestObject(
|
||||
triggerType === Types.afterDelete ||
|
||||
triggerType === Types.beforeLogin ||
|
||||
triggerType === Types.afterLogin ||
|
||||
triggerType === Types.beforePasswordResetRequest ||
|
||||
triggerType === Types.afterFind
|
||||
) {
|
||||
// Set a copy of the context on the request object.
|
||||
|
||||
Reference in New Issue
Block a user