Compare commits
41 Commits
9.1.0-alph
...
9.2.1-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
617de9989b | ||
|
|
d3d6e9e22a | ||
|
|
a4909792bd | ||
|
|
e29910764d | ||
|
|
8cc71cf9e4 | ||
|
|
84959c69e5 | ||
|
|
88b6977333 | ||
|
|
88fa87aa28 | ||
|
|
e70303d5c3 | ||
|
|
9f368ff9ca | ||
|
|
b87eaea12f | ||
|
|
6cfbcfd139 | ||
|
|
c21e8952ae | ||
|
|
f6d78005d4 | ||
|
|
b42a0ee61d | ||
|
|
2457da9e15 | ||
|
|
14b3fce203 | ||
|
|
73e21e77c7 | ||
|
|
c015864293 | ||
|
|
f2babb2ac4 | ||
|
|
9833fdb111 | ||
|
|
dc866bed3b | ||
|
|
906ccc3e29 | ||
|
|
5c00a6ab1b | ||
|
|
5d28fcba0c | ||
|
|
db3cbb2113 | ||
|
|
1d3336d128 | ||
|
|
1b5bd2f754 | ||
|
|
756c204220 | ||
|
|
ba3e7602e6 | ||
|
|
82e0d3ace1 | ||
|
|
69da47284c | ||
|
|
774cc54f81 | ||
|
|
b3725faee2 | ||
|
|
519d798781 | ||
|
|
9f98d3999c | ||
|
|
3d395b3ce5 | ||
|
|
fbcc938b5a | ||
|
|
2e06fa1139 | ||
|
|
8c4d67a0fe | ||
|
|
ae0781d0ac |
5
.github/pull_request_template.md
vendored
5
.github/pull_request_template.md
vendored
@@ -2,10 +2,9 @@
|
||||
|
||||
- Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy).
|
||||
- Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE).
|
||||
- Link this pull request to an [issue](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
## Issue
|
||||
<!-- Add the link to the issue that this PR closes. -->
|
||||
<!-- Describe the issue. -->
|
||||
|
||||
Closes: FILL_THIS_OUT
|
||||
|
||||
@@ -13,7 +12,7 @@ Closes: FILL_THIS_OUT
|
||||
<!-- Describe the changes in this PR. -->
|
||||
|
||||
## Tasks
|
||||
<!-- Delete tasks that don't apply. -->
|
||||
<!-- Check completed tasks and delete tasks that don't apply. -->
|
||||
|
||||
- [ ] Add tests
|
||||
- [ ] Add changes to documentation (guides, repository pages, code comments)
|
||||
|
||||
@@ -605,6 +605,8 @@ This creates a risk that a vulnerability is indirectly disclosed by publishing a
|
||||
|
||||
While the current major version is published on branch `release`, a Long-Term-Support (LTS) version is published on branch `release-#.x.x`, for example `release-4.x.x` for the Parse Server 4.x LTS branch.
|
||||
|
||||
Only the previous major version is under LTS. Older major versions are no longer maintained and their `release-#.x.x` branches are frozen; no further changes will be made. If you need features or fixes on an older branch, fork it and backport changes in your own branch.
|
||||
|
||||
### Preparing Release
|
||||
|
||||
The following changes are done in the `alpha` branch, before publishing the last `beta` version that will eventually become the major release. This way the changes trickle naturally through all branches and code consistency is ensured among branches.
|
||||
|
||||
@@ -1,3 +1,59 @@
|
||||
## [9.2.1-alpha.2](https://github.com/parse-community/parse-server/compare/9.2.1-alpha.1...9.2.1-alpha.2) (2026-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* AuthData validation incorrectly triggered on unchanged providers ([#10025](https://github.com/parse-community/parse-server/issues/10025)) ([d3d6e9e](https://github.com/parse-community/parse-server/commit/d3d6e9e22a212885690853cbbb84bb8c53da5646))
|
||||
|
||||
## [9.2.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.2.0...9.2.1-alpha.1) (2026-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Default HTML pages for password reset, email verification not found ([#10034](https://github.com/parse-community/parse-server/issues/10034)) ([e299107](https://github.com/parse-community/parse-server/commit/e29910764daef3c03ed1b09eee19cedc3b12a86a))
|
||||
|
||||
# [9.2.0-alpha.5](https://github.com/parse-community/parse-server/compare/9.2.0-alpha.4...9.2.0-alpha.5) (2026-02-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade @apollo/server from 5.0.0 to 5.4.0 ([#10035](https://github.com/parse-community/parse-server/issues/10035)) ([9f368ff](https://github.com/parse-community/parse-server/commit/9f368ff9ca322c61cdcfab735e5b5240d1c8f917))
|
||||
|
||||
# [9.2.0-alpha.4](https://github.com/parse-community/parse-server/compare/9.2.0-alpha.3...9.2.0-alpha.4) (2026-01-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade mongodb from 6.20.0 to 7.0.0 ([#10027](https://github.com/parse-community/parse-server/issues/10027)) ([14b3fce](https://github.com/parse-community/parse-server/commit/14b3fce203be0abaf29c27c123cba47f35d09c68))
|
||||
|
||||
# [9.2.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.2.0-alpha.2...9.2.0-alpha.3) (2026-01-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to parse 8.0.3 and @parse/push-adapter 8.2.0 ([#10021](https://github.com/parse-community/parse-server/issues/10021)) ([9833fdb](https://github.com/parse-community/parse-server/commit/9833fdb111c373dc75fc74ea5f9209408186a475))
|
||||
|
||||
# [9.2.0-alpha.2](https://github.com/parse-community/parse-server/compare/9.2.0-alpha.1...9.2.0-alpha.2) (2026-01-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MongoDB timeout errors unhandled and potentially revealing internal data ([#10020](https://github.com/parse-community/parse-server/issues/10020)) ([1d3336d](https://github.com/parse-community/parse-server/commit/1d3336d128671c974b419b9b34db35ada7d1a44d))
|
||||
|
||||
# [9.2.0-alpha.1](https://github.com/parse-community/parse-server/compare/9.1.1...9.2.0-alpha.1) (2026-01-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add option `databaseOptions.clientMetadata` to send custom metadata to database server for logging and debugging ([#10017](https://github.com/parse-community/parse-server/issues/10017)) ([756c204](https://github.com/parse-community/parse-server/commit/756c204220a2c7be3770b7d4a49f11e8903323db))
|
||||
|
||||
## [9.1.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.1.0...9.1.1-alpha.1) (2025-12-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server-Side Request Forgery (SSRF) in Instagram auth adapter [GHSA-3f5f-xgrj-97pf](https://github.com/parse-community/parse-server/security/advisories/GHSA-3f5f-xgrj-97pf) ([#9988](https://github.com/parse-community/parse-server/issues/9988)) ([fbcc938](https://github.com/parse-community/parse-server/commit/fbcc938b5ade5ff4c30598ac51272ef7ecef0616))
|
||||
|
||||
# [9.1.0-alpha.4](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.3...9.1.0-alpha.4) (2025-12-14)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
# [9.2.0](https://github.com/parse-community/parse-server/compare/9.1.1...9.2.0) (2026-02-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MongoDB timeout errors unhandled and potentially revealing internal data ([#10020](https://github.com/parse-community/parse-server/issues/10020)) ([1d3336d](https://github.com/parse-community/parse-server/commit/1d3336d128671c974b419b9b34db35ada7d1a44d))
|
||||
* Security upgrade @apollo/server from 5.0.0 to 5.4.0 ([#10035](https://github.com/parse-community/parse-server/issues/10035)) ([9f368ff](https://github.com/parse-community/parse-server/commit/9f368ff9ca322c61cdcfab735e5b5240d1c8f917))
|
||||
|
||||
### Features
|
||||
|
||||
* Add option `databaseOptions.clientMetadata` to send custom metadata to database server for logging and debugging ([#10017](https://github.com/parse-community/parse-server/issues/10017)) ([756c204](https://github.com/parse-community/parse-server/commit/756c204220a2c7be3770b7d4a49f11e8903323db))
|
||||
* Upgrade mongodb from 6.20.0 to 7.0.0 ([#10027](https://github.com/parse-community/parse-server/issues/10027)) ([14b3fce](https://github.com/parse-community/parse-server/commit/14b3fce203be0abaf29c27c123cba47f35d09c68))
|
||||
* Upgrade to parse 8.0.3 and @parse/push-adapter 8.2.0 ([#10021](https://github.com/parse-community/parse-server/issues/10021)) ([9833fdb](https://github.com/parse-community/parse-server/commit/9833fdb111c373dc75fc74ea5f9209408186a475))
|
||||
|
||||
## [9.1.1](https://github.com/parse-community/parse-server/compare/9.1.0...9.1.1) (2025-12-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server-Side Request Forgery (SSRF) in Instagram auth adapter [GHSA-3f5f-xgrj-97pf](https://github.com/parse-community/parse-server/security/advisories/GHSA-3f5f-xgrj-97pf) ([#9988](https://github.com/parse-community/parse-server/issues/9988)) ([fbcc938](https://github.com/parse-community/parse-server/commit/fbcc938b5ade5ff4c30598ac51272ef7ecef0616))
|
||||
|
||||
# [9.1.0](https://github.com/parse-community/parse-server/compare/9.0.0...9.1.0) (2025-12-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cross-Site Scripting (XSS) via HTML pages for password reset and email verification [GHSA-jhgf-2h8h-ggxv](https://github.com/parse-community/parse-server/security/advisories/GHSA-jhgf-2h8h-ggxv) ([#9985](https://github.com/parse-community/parse-server/issues/9985)) ([3074eb7](https://github.com/parse-community/parse-server/commit/3074eb70f5b58bf72b528ae7b7804ed2d90455ce))
|
||||
|
||||
### Features
|
||||
|
||||
* Add option `logLevels.signupUsernameTaken` to change log level of username already exists sign-up rejection ([#9962](https://github.com/parse-community/parse-server/issues/9962)) ([f18f307](https://github.com/parse-community/parse-server/commit/f18f3073d70a292bc70b5d572ef58e4845de89ca))
|
||||
* Add support for custom HTTP status code and headers to Cloud Function response with Express-style syntax ([#9980](https://github.com/parse-community/parse-server/issues/9980)) ([8eeab8d](https://github.com/parse-community/parse-server/commit/8eeab8dc57edef3751aa188d8247f296a270b083))
|
||||
* Log more debug info when failing to set duplicate value for field with unique values ([#9919](https://github.com/parse-community/parse-server/issues/9919)) ([a23b192](https://github.com/parse-community/parse-server/commit/a23b1924668920f3c92fec0566b57091d0e8aae8))
|
||||
|
||||
# [9.0.0](https://github.com/parse-community/parse-server/compare/8.6.0...9.0.0) (2025-12-14)
|
||||
|
||||
|
||||
|
||||
3218
package-lock.json
generated
3218
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "9.1.0-alpha.4",
|
||||
"version": "9.2.1-alpha.2",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -10,7 +10,7 @@
|
||||
"files": [
|
||||
"bin/",
|
||||
"lib/",
|
||||
"public_html/",
|
||||
"public/",
|
||||
"views/",
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
@@ -20,16 +20,16 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@apollo/server": "5.0.0",
|
||||
"@apollo/server": "5.4.0",
|
||||
"@as-integrations/express5": "1.1.2",
|
||||
"@graphql-tools/merge": "9.0.24",
|
||||
"@graphql-tools/schema": "10.0.23",
|
||||
"@graphql-tools/utils": "10.8.6",
|
||||
"@parse/fs-files-adapter": "3.0.0",
|
||||
"@parse/push-adapter": "8.1.0",
|
||||
"bcryptjs": "3.0.2",
|
||||
"commander": "13.1.0",
|
||||
"cors": "2.8.5",
|
||||
"@parse/push-adapter": "8.2.0",
|
||||
"bcryptjs": "3.0.3",
|
||||
"commander": "14.0.2",
|
||||
"cors": "2.8.6",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "7.5.1",
|
||||
@@ -42,25 +42,25 @@
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"jwks-rsa": "3.2.0",
|
||||
"ldapjs": "3.0.7",
|
||||
"lodash": "4.17.21",
|
||||
"lodash": "4.17.23",
|
||||
"lru-cache": "10.4.0",
|
||||
"mime": "4.0.7",
|
||||
"mongodb": "6.20.0",
|
||||
"mongodb": "7.0.0",
|
||||
"mustache": "4.2.0",
|
||||
"otpauth": "9.4.0",
|
||||
"parse": "8.0.0",
|
||||
"parse": "8.0.3",
|
||||
"path-to-regexp": "8.3.0",
|
||||
"pg-monitor": "3.0.0",
|
||||
"pg-promise": "12.2.0",
|
||||
"pg-promise": "12.6.0",
|
||||
"pluralize": "8.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"rate-limit-redis": "4.2.0",
|
||||
"redis": "4.7.0",
|
||||
"redis": "5.10.0",
|
||||
"semver": "7.7.2",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "11.1.0",
|
||||
"winston": "3.17.0",
|
||||
"winston": "3.19.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.2"
|
||||
},
|
||||
@@ -68,8 +68,8 @@
|
||||
"@actions/core": "1.11.1",
|
||||
"@apollo/client": "3.13.8",
|
||||
"@babel/cli": "7.27.0",
|
||||
"@babel/core": "7.27.4",
|
||||
"@babel/eslint-parser": "7.28.0",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/eslint-parser": "7.28.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.26.5",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
@@ -78,7 +78,7 @@
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/commit-analyzer": "13.0.1",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/github": "11.0.2",
|
||||
"@semantic-release/github": "11.0.3",
|
||||
"@semantic-release/npm": "12.0.1",
|
||||
"@semantic-release/release-notes-generator": "14.0.3",
|
||||
"all-node-versions": "13.0.1",
|
||||
@@ -97,7 +97,7 @@
|
||||
"jsdoc": "4.0.4",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "16.1.0",
|
||||
"m": "1.9.1",
|
||||
"m": "1.10.0",
|
||||
"madge": "8.0.0",
|
||||
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
|
||||
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
|
||||
@@ -108,7 +108,7 @@
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "24.2.5",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.33.1",
|
||||
"typescript-eslint": "8.53.1",
|
||||
"yaml": "2.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -30,6 +30,7 @@ const nestedOptionTypes = [
|
||||
/** The prefix of environment variables for nested options. */
|
||||
const nestedOptionEnvPrefix = {
|
||||
AccountLockoutOptions: 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
|
||||
DatabaseOptionsClientMetadata: 'PARSE_SERVER_DATABASE_CLIENT_METADATA_',
|
||||
CustomPagesOptions: 'PARSE_SERVER_CUSTOM_PAGES_',
|
||||
DatabaseOptions: 'PARSE_SERVER_DATABASE_',
|
||||
FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_',
|
||||
|
||||
@@ -101,6 +101,31 @@ describe('InstagramAdapter', function () {
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore client-provided apiURL and use hardcoded endpoint', async () => {
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
apiURL: 'https://example.com/',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstagramAdapter E2E Test', function () {
|
||||
|
||||
@@ -76,6 +76,41 @@ describe('Auth Adapter features', () => {
|
||||
validateAppId: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
// Code-based adapter that requires 'code' field (like gpgames)
|
||||
const codeBasedAdapter = {
|
||||
validateAppId: () => Promise.resolve(),
|
||||
validateSetUp: authData => {
|
||||
if (!authData.code) {
|
||||
throw new Error('code is required.');
|
||||
}
|
||||
return Promise.resolve({ save: { id: authData.id } });
|
||||
},
|
||||
validateUpdate: authData => {
|
||||
if (!authData.code) {
|
||||
throw new Error('code is required.');
|
||||
}
|
||||
return Promise.resolve({ save: { id: authData.id } });
|
||||
},
|
||||
validateLogin: authData => {
|
||||
if (!authData.code) {
|
||||
throw new Error('code is required.');
|
||||
}
|
||||
return Promise.resolve({ save: { id: authData.id } });
|
||||
},
|
||||
afterFind: authData => {
|
||||
// Strip sensitive 'code' field when returning to client
|
||||
return { id: authData.id };
|
||||
},
|
||||
};
|
||||
|
||||
// Simple adapter that doesn't require code
|
||||
const simpleAdapter = {
|
||||
validateAppId: () => Promise.resolve(),
|
||||
validateSetUp: () => Promise.resolve(),
|
||||
validateUpdate: () => Promise.resolve(),
|
||||
validateLogin: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -1302,4 +1337,42 @@ describe('Auth Adapter features', () => {
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('authData')).toEqual({ adapterB: { id: 'test' } });
|
||||
});
|
||||
|
||||
it('should handle multiple providers: add one while another remains unchanged (code-based)', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
codeBasedAdapter,
|
||||
simpleAdapter,
|
||||
},
|
||||
});
|
||||
|
||||
// Login with code-based provider
|
||||
const user = new Parse.User();
|
||||
await user.save({ authData: { codeBasedAdapter: { id: 'user1', code: 'code1' } } });
|
||||
const sessionToken = user.getSessionToken();
|
||||
await user.fetch({ sessionToken });
|
||||
|
||||
// At this point, authData.codeBasedAdapter only has {id: 'user1'} due to afterFind
|
||||
const current = user.get('authData') || {};
|
||||
expect(current.codeBasedAdapter).toEqual({ id: 'user1' });
|
||||
|
||||
// Add a second provider while keeping the first unchanged
|
||||
user.set('authData', {
|
||||
...current,
|
||||
simpleAdapter: { id: 'simple1' },
|
||||
// codeBasedAdapter is NOT modified (no new code provided)
|
||||
});
|
||||
|
||||
// This should succeed without requiring 'code' for codeBasedAdapter
|
||||
await user.save(null, { sessionToken });
|
||||
|
||||
// Verify both providers are present
|
||||
const reloaded = await new Parse.Query(Parse.User).get(user.id, {
|
||||
useMasterKey: true,
|
||||
});
|
||||
|
||||
const authData = reloaded.get('authData') || {};
|
||||
expect(authData.simpleAdapter && authData.simpleAdapter.id).toBe('simple1');
|
||||
expect(authData.codeBasedAdapter && authData.codeBasedAdapter.id).toBe('user1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -475,4 +475,28 @@ describe_only_db('mongo')('GridFSBucket', () => {
|
||||
expect(e.message).toEqual('Client must be connected before running operations');
|
||||
}
|
||||
});
|
||||
|
||||
describe('MongoDB Client Metadata', () => {
|
||||
it('should not pass metadata to MongoClient by default', async () => {
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
await gfsAdapter._connect();
|
||||
const driverInfo = gfsAdapter._client.s.options.driverInfo;
|
||||
// Either driverInfo should be undefined, or it should not contain our custom metadata
|
||||
if (driverInfo) {
|
||||
expect(driverInfo.name).toBeUndefined();
|
||||
}
|
||||
await gfsAdapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should pass custom metadata to MongoClient when configured', async () => {
|
||||
const customMetadata = { name: 'MyParseServer', version: '1.0.0' };
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI, {
|
||||
clientMetadata: customMetadata
|
||||
});
|
||||
await gfsAdapter._connect();
|
||||
expect(gfsAdapter._client.s.options.driverInfo.name).toBe(customMetadata.name);
|
||||
expect(gfsAdapter._client.s.options.driverInfo.version).toBe(customMetadata.version);
|
||||
await gfsAdapter.handleShutdown();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1063,4 +1063,152 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transient error handling', () => {
|
||||
it('should transform MongoWaitQueueTimeoutError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
await adapter.connect();
|
||||
|
||||
// Create a mock error with the MongoWaitQueueTimeoutError name
|
||||
const mockError = new Error('Timed out while checking out a connection from connection pool');
|
||||
mockError.name = 'MongoWaitQueueTimeoutError';
|
||||
|
||||
try {
|
||||
adapter.handleError(mockError);
|
||||
fail('Expected handleError to throw');
|
||||
} catch (error) {
|
||||
expect(error instanceof Parse.Error).toBe(true);
|
||||
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||
expect(error.message).toBe('Database error');
|
||||
}
|
||||
});
|
||||
|
||||
it('should transform MongoServerSelectionError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
await adapter.connect();
|
||||
|
||||
const mockError = new Error('Server selection timed out');
|
||||
mockError.name = 'MongoServerSelectionError';
|
||||
|
||||
try {
|
||||
adapter.handleError(mockError);
|
||||
fail('Expected handleError to throw');
|
||||
} catch (error) {
|
||||
expect(error instanceof Parse.Error).toBe(true);
|
||||
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||
expect(error.message).toBe('Database error');
|
||||
}
|
||||
});
|
||||
|
||||
it('should transform MongoNetworkTimeoutError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
await adapter.connect();
|
||||
|
||||
const mockError = new Error('Network timeout');
|
||||
mockError.name = 'MongoNetworkTimeoutError';
|
||||
|
||||
try {
|
||||
adapter.handleError(mockError);
|
||||
fail('Expected handleError to throw');
|
||||
} catch (error) {
|
||||
expect(error instanceof Parse.Error).toBe(true);
|
||||
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||
expect(error.message).toBe('Database error');
|
||||
}
|
||||
});
|
||||
|
||||
it('should transform MongoNetworkError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
await adapter.connect();
|
||||
|
||||
const mockError = new Error('Network error');
|
||||
mockError.name = 'MongoNetworkError';
|
||||
|
||||
try {
|
||||
adapter.handleError(mockError);
|
||||
fail('Expected handleError to throw');
|
||||
} catch (error) {
|
||||
expect(error instanceof Parse.Error).toBe(true);
|
||||
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||
expect(error.message).toBe('Database error');
|
||||
}
|
||||
});
|
||||
|
||||
it('should transform TransientTransactionError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
await adapter.connect();
|
||||
|
||||
const mockError = new Error('Transient transaction error');
|
||||
mockError.hasErrorLabel = label => label === 'TransientTransactionError';
|
||||
|
||||
try {
|
||||
adapter.handleError(mockError);
|
||||
fail('Expected handleError to throw');
|
||||
} catch (error) {
|
||||
expect(error instanceof Parse.Error).toBe(true);
|
||||
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||
expect(error.message).toBe('Database error');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not transform non-transient errors', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
await adapter.connect();
|
||||
|
||||
const mockError = new Error('Some other error');
|
||||
mockError.name = 'SomeOtherError';
|
||||
|
||||
try {
|
||||
adapter.handleError(mockError);
|
||||
fail('Expected handleError to throw');
|
||||
} catch (error) {
|
||||
expect(error instanceof Parse.Error).toBe(false);
|
||||
expect(error.message).toBe('Some other error');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle null/undefined errors', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
await adapter.connect();
|
||||
|
||||
try {
|
||||
adapter.handleError(null);
|
||||
fail('Expected handleError to throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeNull();
|
||||
}
|
||||
|
||||
try {
|
||||
adapter.handleError(undefined);
|
||||
fail('Expected handleError to throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MongoDB Client Metadata', () => {
|
||||
it('should not pass metadata to MongoClient by default', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
await adapter.connect();
|
||||
const driverInfo = adapter.client.s.options.driverInfo;
|
||||
// Either driverInfo should be undefined, or it should not contain our custom metadata
|
||||
if (driverInfo) {
|
||||
expect(driverInfo.name).toBeUndefined();
|
||||
}
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should pass custom metadata to MongoClient when configured', async () => {
|
||||
const customMetadata = { name: 'MyParseServer', version: '1.0.0' };
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { clientMetadata: customMetadata }
|
||||
});
|
||||
await adapter.connect();
|
||||
expect(adapter.client.s.options.driverInfo.name).toBe(customMetadata.name);
|
||||
expect(adapter.client.s.options.driverInfo.version).toBe(customMetadata.version);
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1181,6 +1181,61 @@ describe('Pages Router', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('async publicServerURL', () => {
|
||||
it('resolves async publicServerURL for password reset page', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appId: 'test',
|
||||
appName: 'exampleAppname',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
publicServerURL: () => 'http://localhost:8378/1',
|
||||
pages: { enableRouter: true },
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('asyncUrlUser');
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'async-url@example.com');
|
||||
await user.signUp();
|
||||
await Parse.User.requestPasswordReset('async-url@example.com');
|
||||
|
||||
const response = await request({
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset?token=invalidToken',
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toContain('Invalid password reset link!');
|
||||
});
|
||||
|
||||
it('resolves async publicServerURL for email verification page', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appId: 'test',
|
||||
appName: 'exampleAppname',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
publicServerURL: () => 'http://localhost:8378/1',
|
||||
pages: { enableRouter: true },
|
||||
});
|
||||
|
||||
const response = await request({
|
||||
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken',
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toContain('Invalid verification link!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('XSS Protection', () => {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('Server Url Checks', () => {
|
||||
parseServerProcess.on('close', async code => {
|
||||
expect(code).toEqual(1);
|
||||
expect(stdout).not.toContain('UnhandledPromiseRejectionWarning');
|
||||
expect(stderr).toContain('MongoServerSelectionError');
|
||||
expect(stderr).toContain('Database error');
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -21,7 +21,6 @@ describe('RedisPubSub', function () {
|
||||
expect(redis.createClient).toHaveBeenCalledWith({
|
||||
url: 'redisAddress',
|
||||
socket_keepalive: true,
|
||||
no_ready_check: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +34,6 @@ describe('RedisPubSub', function () {
|
||||
expect(redis.createClient).toHaveBeenCalledWith({
|
||||
url: 'redisAddress',
|
||||
socket_keepalive: true,
|
||||
no_ready_check: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('server', () => {
|
||||
}),
|
||||
});
|
||||
const error = await server.start().catch(e => e);
|
||||
expect(`${error}`.includes('MongoServerSelectionError')).toBeTrue();
|
||||
expect(`${error}`.includes('Database error')).toBeTrue();
|
||||
await reconfigureServer();
|
||||
});
|
||||
|
||||
|
||||
@@ -96,8 +96,7 @@ class InstagramAdapter extends BaseAuthCodeAdapter {
|
||||
}
|
||||
|
||||
async getUserFromAccessToken(accessToken, authData) {
|
||||
const defaultURL = 'https://graph.instagram.com/';
|
||||
const apiURL = authData.apiURL || defaultURL;
|
||||
const apiURL = 'https://graph.instagram.com/';
|
||||
const path = `${apiURL}me?fields=id&access_token=${accessToken}`;
|
||||
|
||||
const response = await fetch(path);
|
||||
|
||||
@@ -35,7 +35,7 @@ export class RedisCacheAdapter {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.client.quit();
|
||||
await this.client.close();
|
||||
} catch (err) {
|
||||
logger.error('RedisCacheAdapter error on shutdown', { error: err });
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
_connectionPromise: Promise<Db>;
|
||||
_mongoOptions: Object;
|
||||
_algorithm: string;
|
||||
_clientMetadata: ?{ name: string, version: string };
|
||||
|
||||
constructor(
|
||||
mongoDatabaseURI = defaults.DefaultMongoURI,
|
||||
@@ -36,6 +37,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
: null;
|
||||
const defaultMongoOptions = {};
|
||||
const _mongoOptions = Object.assign(defaultMongoOptions, mongoOptions);
|
||||
this._clientMetadata = mongoOptions.clientMetadata;
|
||||
// Remove Parse Server-specific options that should not be passed to MongoDB client
|
||||
for (const key of ParseServerDatabaseOptions) {
|
||||
delete _mongoOptions[key];
|
||||
@@ -45,7 +47,16 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
|
||||
_connect() {
|
||||
if (!this._connectionPromise) {
|
||||
this._connectionPromise = MongoClient.connect(this._databaseURI, this._mongoOptions).then(
|
||||
// Only use driverInfo if clientMetadata option is set
|
||||
const options = { ...this._mongoOptions };
|
||||
if (this._clientMetadata) {
|
||||
options.driverInfo = {
|
||||
name: this._clientMetadata.name,
|
||||
version: this._clientMetadata.version
|
||||
};
|
||||
}
|
||||
|
||||
this._connectionPromise = MongoClient.connect(this._databaseURI, options).then(
|
||||
client => {
|
||||
this._client = client;
|
||||
return client.db(client.s.options.dbName);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createClient } from 'redis';
|
||||
import { logger } from '../../logger';
|
||||
|
||||
function createPublisher({ redisURL, redisOptions = {} }): any {
|
||||
redisOptions.no_ready_check = true;
|
||||
const client = createClient({ url: redisURL, ...redisOptions });
|
||||
client.on('error', err => { logger.error('RedisPubSub Publisher client error', { error: err }) });
|
||||
client.on('connect', () => {});
|
||||
@@ -12,7 +11,6 @@ function createPublisher({ redisURL, redisOptions = {} }): any {
|
||||
}
|
||||
|
||||
function createSubscriber({ redisURL, redisOptions = {} }): any {
|
||||
redisOptions.no_ready_check = true;
|
||||
const client = createClient({ url: redisURL, ...redisOptions });
|
||||
client.on('error', err => { logger.error('RedisPubSub Subscriber client error', { error: err }) });
|
||||
client.on('connect', () => {});
|
||||
|
||||
@@ -27,6 +27,36 @@ const ReadPreference = mongodb.ReadPreference;
|
||||
|
||||
const MongoSchemaCollectionName = '_SCHEMA';
|
||||
|
||||
/**
|
||||
* Determines if a MongoDB error is a transient infrastructure error
|
||||
* (connection pool, network, server selection) as opposed to a query-level error.
|
||||
*/
|
||||
function isTransientError(error) {
|
||||
if (!error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Connection pool, network, and server selection errors
|
||||
const transientErrorNames = [
|
||||
'MongoWaitQueueTimeoutError',
|
||||
'MongoServerSelectionError',
|
||||
'MongoNetworkTimeoutError',
|
||||
'MongoNetworkError',
|
||||
];
|
||||
if (transientErrorNames.includes(error.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for MongoDB's transient transaction error label
|
||||
if (typeof error.hasErrorLabel === 'function') {
|
||||
if (error.hasErrorLabel('TransientTransactionError')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const storageAdapterAllCollections = mongoAdapter => {
|
||||
return mongoAdapter
|
||||
.connect()
|
||||
@@ -134,6 +164,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
_onchange: any;
|
||||
_stream: any;
|
||||
_logClientEvents: ?Array<any>;
|
||||
_clientMetadata: ?{ name: string, version: string };
|
||||
// Public
|
||||
connectionPromise: ?Promise<any>;
|
||||
database: any;
|
||||
@@ -156,6 +187,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
this.schemaCacheTtl = mongoOptions.schemaCacheTtl;
|
||||
this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation;
|
||||
this._logClientEvents = mongoOptions.logClientEvents;
|
||||
this._clientMetadata = mongoOptions.clientMetadata;
|
||||
|
||||
// Create a copy of mongoOptions and remove Parse Server-specific options that should not
|
||||
// be passed to MongoDB client. Note: We only delete from this._mongoOptions, not from the
|
||||
@@ -179,7 +211,17 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// parsing and re-formatting causes the auth value (if there) to get URI
|
||||
// encoded
|
||||
const encodedUri = formatUrl(parseUrl(this._uri));
|
||||
this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions)
|
||||
|
||||
// Only use driverInfo if clientMetadata option is set
|
||||
const options = { ...this._mongoOptions };
|
||||
if (this._clientMetadata) {
|
||||
options.driverInfo = {
|
||||
name: this._clientMetadata.name,
|
||||
version: this._clientMetadata.version
|
||||
};
|
||||
}
|
||||
|
||||
this.connectionPromise = MongoClient.connect(encodedUri, options)
|
||||
.then(client => {
|
||||
// Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client
|
||||
// Fortunately, we can get back the options and use them to select the proper DB.
|
||||
@@ -240,6 +282,13 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
delete this.connectionPromise;
|
||||
logger.error('Received unauthorized error', { error: error });
|
||||
}
|
||||
|
||||
// Transform infrastructure/transient errors into Parse.Error.INTERNAL_SERVER_ERROR
|
||||
if (isTransientError(error)) {
|
||||
logger.error('Database transient error', error);
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database error');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
27
src/Auth.js
27
src/Auth.js
@@ -456,7 +456,32 @@ const hasMutatedAuthData = (authData, userAuthData) => {
|
||||
if (provider === 'anonymous') { return; }
|
||||
const providerData = authData[provider];
|
||||
const userProviderAuthData = userAuthData[provider];
|
||||
if (!isDeepStrictEqual(providerData, userProviderAuthData)) {
|
||||
|
||||
// If unlinking (setting to null), consider it mutated
|
||||
if (providerData === null) {
|
||||
mutatedAuthData[provider] = providerData;
|
||||
return;
|
||||
}
|
||||
|
||||
// If provider doesn't exist in stored data, it's new
|
||||
if (!userProviderAuthData) {
|
||||
mutatedAuthData[provider] = providerData;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if incoming data represents actual changes vs just echoing back
|
||||
// what afterFind returned. If incoming data is a subset of stored data
|
||||
// (all incoming fields match stored values), it's not mutated.
|
||||
// If incoming data has different values or fields not in stored data, it's mutated.
|
||||
// This handles the case where afterFind strips sensitive fields like 'code':
|
||||
// - Incoming: { id: 'x' }, Stored: { id: 'x', code: 'secret' } -> NOT mutated (subset)
|
||||
// - Incoming: { id: 'x', token: 'new' }, Stored: { id: 'x', token: 'old' } -> MUTATED
|
||||
const incomingKeys = Object.keys(providerData || {});
|
||||
const hasChanges = incomingKeys.some(key => {
|
||||
return !isDeepStrictEqual(providerData[key], userProviderAuthData[key]);
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
mutatedAuthData[provider] = providerData;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,9 +109,9 @@ class ParseLiveQueryServer {
|
||||
this.subscriber.close?.(),
|
||||
]);
|
||||
}
|
||||
if (typeof this.subscriber.quit === 'function') {
|
||||
if (typeof this.subscriber.close === 'function') {
|
||||
try {
|
||||
await this.subscriber.quit();
|
||||
await this.subscriber.close();
|
||||
} catch (err) {
|
||||
logger.error('PubSubAdapter error on shutdown', { error: err });
|
||||
}
|
||||
|
||||
@@ -1179,6 +1179,13 @@ module.exports.DatabaseOptions = {
|
||||
'The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.',
|
||||
action: parsers.numberParser('autoSelectFamilyAttemptTimeout'),
|
||||
},
|
||||
clientMetadata: {
|
||||
env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA',
|
||||
help:
|
||||
"Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead.",
|
||||
action: parsers.objectParser,
|
||||
type: 'DatabaseOptionsClientMetadata',
|
||||
},
|
||||
compressors: {
|
||||
env: 'PARSE_SERVER_DATABASE_COMPRESSORS',
|
||||
help:
|
||||
@@ -1461,6 +1468,18 @@ module.exports.DatabaseOptions = {
|
||||
action: parsers.numberParser('zlibCompressionLevel'),
|
||||
},
|
||||
};
|
||||
module.exports.DatabaseOptionsClientMetadata = {
|
||||
name: {
|
||||
env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA_NAME',
|
||||
help: "The name to identify your application in database logs (e.g., 'MyApp').",
|
||||
required: true,
|
||||
},
|
||||
version: {
|
||||
env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA_VERSION',
|
||||
help: "The version of your application (e.g., '1.0.0').",
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
module.exports.AuthAdapter = {
|
||||
enabled: {
|
||||
help: 'Is `true` if the auth adapter is enabled, `false` otherwise.',
|
||||
|
||||
@@ -264,6 +264,7 @@
|
||||
* @property {String} authSource The MongoDB driver option to specify the database name associated with the user's credentials.
|
||||
* @property {Boolean} autoSelectFamily The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address.
|
||||
* @property {Number} autoSelectFamilyAttemptTimeout The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.
|
||||
* @property {DatabaseOptionsClientMetadata} clientMetadata Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead.
|
||||
* @property {Union} compressors The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance.
|
||||
* @property {Number} connectTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout.
|
||||
* @property {Boolean} createIndexRoleName Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
|
||||
@@ -315,6 +316,12 @@
|
||||
* @property {Number} zlibCompressionLevel The MongoDB driver option to specify the compression level if using zlib for network compression (0-9).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface DatabaseOptionsClientMetadata
|
||||
* @property {String} name The name to identify your application in database logs (e.g., 'MyApp').
|
||||
* @property {String} version The version of your application (e.g., '1.0.0').
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface AuthAdapter
|
||||
* @property {Boolean} enabled Is `true` if the auth adapter is enabled, `false` otherwise.
|
||||
|
||||
@@ -755,6 +755,15 @@ export interface DatabaseOptions {
|
||||
allowPublicExplain: ?boolean;
|
||||
/* An array of MongoDB client event configurations to enable logging of specific events. */
|
||||
logClientEvents: ?(LogClientEvent[]);
|
||||
/* Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead. */
|
||||
clientMetadata: ?DatabaseOptionsClientMetadata;
|
||||
}
|
||||
|
||||
export interface DatabaseOptionsClientMetadata {
|
||||
/* The name to identify your application in database logs (e.g., 'MyApp'). */
|
||||
name: string;
|
||||
/* The version of your application (e.g., '1.0.0'). */
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface AuthAdapter {
|
||||
|
||||
@@ -624,12 +624,14 @@ export class PagesRouter extends PromiseRouter {
|
||||
* @param {Boolean} failGracefully Is true if failing to set the config should
|
||||
* not result in an invalid request response. Default is `false`.
|
||||
*/
|
||||
setConfig(req, failGracefully = false) {
|
||||
async setConfig(req, failGracefully = false) {
|
||||
req.config = Config.get(req.params.appId || req.query.appId);
|
||||
if (!req.config && !failGracefully) {
|
||||
this.invalidRequest();
|
||||
}
|
||||
return Promise.resolve();
|
||||
if (req.config) {
|
||||
await req.config.loadKeys();
|
||||
}
|
||||
}
|
||||
|
||||
mountPagesRoutes() {
|
||||
@@ -637,7 +639,7 @@ export class PagesRouter extends PromiseRouter {
|
||||
'GET',
|
||||
`/${this.pagesEndpoint}/:appId/verify_email`,
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
return this.setConfig(req);
|
||||
},
|
||||
req => {
|
||||
return this.verifyEmail(req);
|
||||
@@ -648,7 +650,7 @@ export class PagesRouter extends PromiseRouter {
|
||||
'POST',
|
||||
`/${this.pagesEndpoint}/:appId/resend_verification_email`,
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
return this.setConfig(req);
|
||||
},
|
||||
req => {
|
||||
return this.resendVerificationEmail(req);
|
||||
@@ -659,7 +661,7 @@ export class PagesRouter extends PromiseRouter {
|
||||
'GET',
|
||||
`/${this.pagesEndpoint}/choose_password`,
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
return this.setConfig(req);
|
||||
},
|
||||
req => {
|
||||
return this.passwordReset(req);
|
||||
@@ -670,7 +672,7 @@ export class PagesRouter extends PromiseRouter {
|
||||
'POST',
|
||||
`/${this.pagesEndpoint}/:appId/request_password_reset`,
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
return this.setConfig(req);
|
||||
},
|
||||
req => {
|
||||
return this.resetPassword(req);
|
||||
@@ -681,7 +683,7 @@ export class PagesRouter extends PromiseRouter {
|
||||
'GET',
|
||||
`/${this.pagesEndpoint}/:appId/request_password_reset`,
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
return this.setConfig(req);
|
||||
},
|
||||
req => {
|
||||
return this.requestResetPassword(req);
|
||||
@@ -695,7 +697,7 @@ export class PagesRouter extends PromiseRouter {
|
||||
route.method,
|
||||
`/${this.pagesEndpoint}/:appId/${route.path}`,
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
return this.setConfig(req);
|
||||
},
|
||||
async req => {
|
||||
const { file, query = {} } = (await route.handler(req)) || {};
|
||||
@@ -718,7 +720,7 @@ export class PagesRouter extends PromiseRouter {
|
||||
'GET',
|
||||
`/${this.pagesEndpoint}/*resource`,
|
||||
req => {
|
||||
this.setConfig(req, true);
|
||||
return this.setConfig(req, true);
|
||||
},
|
||||
req => {
|
||||
return this.staticRoute(req);
|
||||
|
||||
@@ -38,6 +38,7 @@ export const DefaultMongoURI = DefinitionDefaults.databaseURI;
|
||||
// before passing to MongoDB client
|
||||
export const ParseServerDatabaseOptions = [
|
||||
'allowPublicExplain',
|
||||
'clientMetadata',
|
||||
'createIndexRoleName',
|
||||
'createIndexUserEmail',
|
||||
'createIndexUserEmailCaseInsensitive',
|
||||
|
||||
@@ -378,9 +378,9 @@ export const handleParseSession = async (req, res, next) => {
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
// TODO: Determine the correct error scenario.
|
||||
// Log full error details internally, but don't expose to client
|
||||
req.config.loggerController.error('error getting auth for sessionToken', error);
|
||||
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
|
||||
next(new Parse.Error(Parse.Error.UNKNOWN_ERROR, 'Unknown error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user