49 Commits

Author SHA1 Message Date
semantic-release-bot
27b27a7f5c chore(release): 9.3.0-alpha.1 [skip ci]
# [9.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/9.2.1-alpha.2...9.3.0-alpha.1) (2026-02-06)

### Features

* Add event information to `verifyUserEmails`, `preventLoginWithUnverifiedEmail` to identify invoking signup / login action and auth provider ([#9963](https://github.com/parse-community/parse-server/issues/9963)) ([ed98c15](ed98c15f90))
2026-02-06 03:49:32 +00:00
Palixir
ed98c15f90 feat: Add event information to verifyUserEmails, preventLoginWithUnverifiedEmail to identify invoking signup / login action and auth provider (#9963) 2026-02-06 03:48:35 +00:00
semantic-release-bot
617de9989b chore(release): 9.2.1-alpha.2 [skip ci]
## [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](d3d6e9e22a))
2026-02-06 02:04:22 +00:00
Copilot
d3d6e9e22a fix: AuthData validation incorrectly triggered on unchanged providers (#10025) 2026-02-06 02:03:34 +00:00
semantic-release-bot
a4909792bd chore(release): 9.2.1-alpha.1 [skip ci]
## [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](e29910764d))
2026-02-06 01:43:56 +00:00
Manuel
e29910764d fix: Default HTML pages for password reset, email verification not found (#10034) 2026-02-06 01:42:54 +00:00
dependabot[bot]
8cc71cf9e4 refactor: Bump @babel/core from 7.28.6 to 7.29.0 (#10032) 2026-02-05 13:44:33 +00:00
semantic-release-bot
84959c69e5 chore(release): 9.2.0 [skip ci]
# [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](1d3336d128))
* Security upgrade @apollo/server from 5.0.0 to 5.4.0 ([#10035](https://github.com/parse-community/parse-server/issues/10035)) ([9f368ff](9f368ff9ca))

### 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](756c204220))
* Upgrade mongodb from 6.20.0 to 7.0.0 ([#10027](https://github.com/parse-community/parse-server/issues/10027)) ([14b3fce](14b3fce203))
* Upgrade to parse 8.0.3 and @parse/push-adapter 8.2.0 ([#10021](https://github.com/parse-community/parse-server/issues/10021)) ([9833fdb](9833fdb111))
2026-02-05 13:10:33 +00:00
Manuel
88b6977333 build: Release (#10036) 2026-02-05 13:09:31 +00:00
GitHub Actions
88fa87aa28 empty commit to trigger CI 2026-02-05 12:45:46 +00:00
semantic-release-bot
e70303d5c3 chore(release): 9.2.0-alpha.5 [skip ci]
# [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](9f368ff9ca))
2026-02-05 12:43:01 +00:00
dependabot[bot]
9f368ff9ca fix: Security upgrade @apollo/server from 5.0.0 to 5.4.0 (#10035) 2026-02-05 12:42:13 +00:00
dependabot[bot]
b87eaea12f refactor: Bump cors from 2.8.5 to 2.8.6 (#10033) 2026-02-04 17:55:30 +00:00
Corey
6cfbcfd139 refactor: Upgrade pg-promise from 12.2.0 to 12.6.0 (#10031) 2026-02-04 00:55:44 +00:00
dependabot[bot]
c21e8952ae refactor: Bump m from 1.9.1 to 1.10.0 (#10029) 2026-01-31 02:22:17 +01:00
dependabot[bot]
f6d78005d4 refactor: Bump @babel/eslint-parser from 7.28.0 to 7.28.6 (#10028) 2026-01-30 19:16:58 +01:00
dependabot[bot]
b42a0ee61d refactor: Bump winston from 3.17.0 to 3.19.0 (#10023) 2026-01-29 20:29:19 +01:00
semantic-release-bot
2457da9e15 chore(release): 9.2.0-alpha.4 [skip ci]
# [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](14b3fce203))
2026-01-29 12:52:21 +00:00
dependabot[bot]
14b3fce203 feat: Upgrade mongodb from 6.20.0 to 7.0.0 (#10027) 2026-01-29 13:51:32 +01:00
dependabot[bot]
73e21e77c7 refactor: Bump bcryptjs from 3.0.2 to 3.0.3 (#10022) 2026-01-27 20:09:50 +01:00
Manuel
c015864293 docs: Simplify PR template (#10026) 2026-01-27 18:07:02 +01:00
semantic-release-bot
f2babb2ac4 chore(release): 9.2.0-alpha.3 [skip ci]
# [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](9833fdb111))
2026-01-27 17:02:53 +00:00
Manuel
9833fdb111 feat: Upgrade to parse 8.0.3 and @parse/push-adapter 8.2.0 (#10021) 2026-01-27 18:01:57 +01:00
dependabot[bot]
dc866bed3b refactor: Bump lodash from 4.17.21 to 4.17.23 (#10016) 2026-01-25 19:46:36 +01:00
dependabot[bot]
906ccc3e29 refactor: Bump lodash-es from 4.17.21 to 4.17.23 (#10015) 2026-01-25 19:25:54 +01:00
dependabot[bot]
5c00a6ab1b refactor: Bump typescript-eslint from 8.33.1 to 8.53.0 (#10011) 2026-01-25 18:58:41 +01:00
dependabot[bot]
5d28fcba0c refactor: Bump @babel/core from 7.27.4 to 7.28.6 (#10012) 2026-01-25 18:58:15 +01:00
semantic-release-bot
db3cbb2113 chore(release): 9.2.0-alpha.2 [skip ci]
# [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](1d3336d128))
2026-01-24 23:15:52 +00:00
Manuel
1d3336d128 fix: MongoDB timeout errors unhandled and potentially revealing internal data (#10020) 2026-01-25 00:15:01 +01:00
semantic-release-bot
1b5bd2f754 chore(release): 9.2.0-alpha.1 [skip ci]
# [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](756c204220))
2026-01-24 21:45:25 +00:00
Copilot
756c204220 feat: Add option databaseOptions.clientMetadata to send custom metadata to database server for logging and debugging (#10017) 2026-01-24 22:44:38 +01:00
dependabot[bot]
ba3e7602e6 refactor: Bump redis from 4.7.0 to 5.10.0 (#9994) 2026-01-17 22:09:44 +01:00
dependabot[bot]
82e0d3ace1 refactor: Bump commander from 13.1.0 to 14.0.2 (#9992) 2026-01-17 19:52:59 +01:00
Manuel
69da47284c docs: Add frozen LTS branch info to CONTRIBUTING guide (#10008) 2026-01-16 05:05:27 +01:00
dependabot[bot]
774cc54f81 refactor: Bump @semantic-release/github from 11.0.2 to 11.0.3 (#9789) 2025-12-25 14:23:57 +01:00
semantic-release-bot
b3725faee2 chore(release): 9.1.1 [skip ci]
## [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](fbcc938b5a))
2025-12-16 01:34:54 +00:00
Manuel
519d798781 build: Release (#9990) 2025-12-16 02:34:01 +01:00
GitHub Actions
9f98d3999c empty commit to trigger CI 2025-12-16 01:32:07 +00:00
semantic-release-bot
3d395b3ce5 chore(release): 9.1.1-alpha.1 [skip ci]
## [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](fbcc938b5a))
2025-12-16 01:25:34 +00:00
Manuel
fbcc938b5a fix: 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) 2025-12-16 02:24:37 +01:00
semantic-release-bot
2e06fa1139 chore(release): 9.1.0 [skip ci]
# [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](3074eb70f5))

### 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](f18f3073d7))
* 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](8eeab8dc57))
* 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](a23b192466))
2025-12-14 15:49:33 +00:00
Manuel
8c4d67a0fe build: Release (#9987) 2025-12-14 16:48:45 +01:00
GitHub Actions
ae0781d0ac empty commit to trigger CI 2025-12-14 15:41:22 +00:00
semantic-release-bot
0e308feaa7 chore(release): 9.1.0-alpha.4 [skip ci]
# [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)

### Features

* 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](a23b192466))
2025-12-14 15:40:04 +00:00
Rahul Lanjewar
a23b192466 feat: Log more debug info when failing to set duplicate value for field with unique values (#9919) 2025-12-14 16:39:17 +01:00
semantic-release-bot
98a42e5277 chore(release): 9.1.0-alpha.3 [skip ci]
# [9.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.2...9.1.0-alpha.3) (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](3074eb70f5))
2025-12-14 14:45:10 +00:00
Manuel
3074eb70f5 fix: 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) 2025-12-14 15:44:04 +01:00
semantic-release-bot
7028e0385c chore(release): 9.1.0-alpha.2 [skip ci]
# [9.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.1...9.1.0-alpha.2) (2025-12-14)

### Features

* 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](8eeab8dc57))
2025-12-14 14:25:39 +00:00
Copilot
8eeab8dc57 feat: Add support for custom HTTP status code and headers to Cloud Function response with Express-style syntax (#9980) 2025-12-14 15:24:51 +01:00
46 changed files with 3079 additions and 1629 deletions

View File

@@ -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)

View File

@@ -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.

View File

@@ -1,3 +1,87 @@
# [9.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/9.2.1-alpha.2...9.3.0-alpha.1) (2026-02-06)
### Features
* Add event information to `verifyUserEmails`, `preventLoginWithUnverifiedEmail` to identify invoking signup / login action and auth provider ([#9963](https://github.com/parse-community/parse-server/issues/9963)) ([ed98c15](https://github.com/parse-community/parse-server/commit/ed98c15f90f2fa6a66780941fd3705b805d6eb14))
## [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)
### Features
* 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.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.2...9.1.0-alpha.3) (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))
# [9.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.1...9.1.0-alpha.2) (2025-12-14)
### Features
* 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))
# [9.1.0-alpha.1](https://github.com/parse-community/parse-server/compare/9.0.0...9.1.0-alpha.1) (2025-12-14)

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "9.1.0-alpha.1",
"version": "9.3.0-alpha.1",
"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": {

View File

@@ -14,9 +14,9 @@
<body>
<h1>{{appName}}</h1>
<h1>Expired verification link!</h1>
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
<input name="token" type="hidden" value="{{{token}}}">
<input name="locale" type="hidden" value="{{{locale}}}">
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
<input name="token" type="hidden" value="{{token}}">
<input name="locale" type="hidden" value="{{locale}}">
<button type="submit">Resend Link</button>
</form>
</body>

View File

@@ -23,11 +23,11 @@
<p>You can set a new Password for your account: {{username}}</p>
<br />
<p>{{error}}</p>
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
<input name='utf-8' type='hidden' value='✓' />
<input name="username" type="hidden" id="username" value="{{{username}}}" />
<input name="token" type="hidden" id="token" value="{{{token}}}" />
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
<input name="username" type="hidden" id="username" value="{{username}}" />
<input name="token" type="hidden" id="token" value="{{token}}" />
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
<p>New Password</p>
<input name="new_password" type="password" id="password" />

View File

@@ -14,9 +14,9 @@
<body>
<h1>{{appName}}</h1>
<h1>Expired verification link!</h1>
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
<input name="token" type="hidden" value="{{{token}}}">
<input name="locale" type="hidden" value="{{{locale}}}">
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
<input name="token" type="hidden" value="{{token}}">
<input name="locale" type="hidden" value="{{locale}}">
<button type="submit">Resend Link</button>
</form>
</body>

View File

@@ -23,11 +23,11 @@
<p>You can set a new Password for your account: {{username}}</p>
<br />
<p>{{error}}</p>
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
<input name='utf-8' type='hidden' value='✓' />
<input name="username" type="hidden" id="username" value="{{{username}}}" />
<input name="token" type="hidden" id="token" value="{{{token}}}" />
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
<input name="username" type="hidden" id="username" value="{{username}}" />
<input name="token" type="hidden" id="token" value="{{token}}" />
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
<p>New Password</p>
<input name="new_password" type="password" id="password" />

View File

@@ -14,9 +14,9 @@
<body>
<h1>{{appName}}</h1>
<h1>Expired verification link!</h1>
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
<input name="token" type="hidden" value="{{{token}}}">
<input name="locale" type="hidden" value="{{{locale}}}">
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
<input name="token" type="hidden" value="{{token}}">
<input name="locale" type="hidden" value="{{locale}}">
<button type="submit">Resend Link</button>
</form>
</body>

View File

@@ -23,11 +23,11 @@
<p>You can set a new Password for your account: {{username}}</p>
<br />
<p>{{error}}</p>
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
<input name='utf-8' type='hidden' value='✓' />
<input name="username" type="hidden" id="username" value="{{{username}}}" />
<input name="token" type="hidden" id="token" value="{{{token}}}" />
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
<input name="username" type="hidden" id="username" value="{{username}}" />
<input name="token" type="hidden" id="token" value="{{token}}" />
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
<p>New Password</p>
<input name="new_password" type="password" id="password" />

View File

@@ -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_',
@@ -157,6 +158,11 @@ function mapperFor(elt, t) {
return wrap(t.identifier('booleanParser'));
} else if (t.isObjectTypeAnnotation(elt)) {
return wrap(t.identifier('objectParser'));
} else if (t.isUnionTypeAnnotation(elt)) {
const unionTypes = elt.typeAnnotation?.types || elt.types;
if (unionTypes?.some(type => t.isBooleanTypeAnnotation(type)) && unionTypes?.some(type => t.isFunctionTypeAnnotation(type))) {
return wrap(t.identifier('booleanOrFunctionParser'));
}
} else if (t.isGenericTypeAnnotation(elt)) {
const type = elt.typeAnnotation.id.name;
if (type == 'Adapter') {

View File

@@ -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 () {

View File

@@ -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');
});
});

View File

@@ -4788,4 +4788,231 @@ describe('beforePasswordResetRequest hook', () => {
Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { });
}).not.toThrow();
});
describe('Express-style cloud functions with (req, res) parameters', () => {
it('should support express-style cloud function with res.success()', async () => {
Parse.Cloud.define('expressStyleFunction', (req, res) => {
res.success({ message: 'Hello from express style!' });
});
const result = await Parse.Cloud.run('expressStyleFunction', {});
expect(result.message).toEqual('Hello from express style!');
});
it('should support express-style cloud function with res.error()', async () => {
Parse.Cloud.define('expressStyleError', (req, res) => {
res.error('Custom error message');
});
await expectAsync(Parse.Cloud.run('expressStyleError', {})).toBeRejectedWith(
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Custom error message')
);
});
it('should support setting custom HTTP status code with res.status().success()', async () => {
Parse.Cloud.define('customStatusCode', (req, res) => {
res.status(201).success({ created: true });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/customStatusCode',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(201);
expect(response.data.result.created).toBe(true);
});
it('should support 401 unauthorized status code with error', async () => {
Parse.Cloud.define('unauthorizedFunction', (req, res) => {
if (!req.user) {
res.status(401).error('Unauthorized access');
} else {
res.success({ message: 'Authorized' });
}
});
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/functions/unauthorizedFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
})
).toBeRejected();
});
it('should support 404 not found status code with error', async () => {
Parse.Cloud.define('notFoundFunction', (req, res) => {
res.status(404).error('Resource not found');
});
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/functions/notFoundFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
})
).toBeRejected();
});
it('should default to 200 status code when not specified', async () => {
Parse.Cloud.define('defaultStatusCode', (req, res) => {
res.success({ message: 'Default status' });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/defaultStatusCode',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(200);
expect(response.data.result.message).toBe('Default status');
});
it('should maintain backward compatibility with single-parameter functions', async () => {
Parse.Cloud.define('traditionalFunction', (req) => {
return { message: 'Traditional style works!' };
});
const result = await Parse.Cloud.run('traditionalFunction', {});
expect(result.message).toEqual('Traditional style works!');
});
it('should maintain backward compatibility with implicit return functions', async () => {
Parse.Cloud.define('implicitReturnFunction', () => 'Implicit return works!');
const result = await Parse.Cloud.run('implicitReturnFunction', {});
expect(result).toEqual('Implicit return works!');
});
it('should support async express-style functions', async () => {
Parse.Cloud.define('asyncExpressStyle', async (req, res) => {
await new Promise(resolve => setTimeout(resolve, 10));
res.success({ async: true });
});
const result = await Parse.Cloud.run('asyncExpressStyle', {});
expect(result.async).toBe(true);
});
it('should access request parameters in express-style functions', async () => {
Parse.Cloud.define('expressWithParams', (req, res) => {
const { name } = req.params;
res.success({ greeting: `Hello, ${name}!` });
});
const result = await Parse.Cloud.run('expressWithParams', { name: 'World' });
expect(result.greeting).toEqual('Hello, World!');
});
it('should access user in express-style functions', async () => {
const user = new Parse.User();
user.set('username', 'testuser');
user.set('password', 'testpass');
await user.signUp();
Parse.Cloud.define('expressWithUser', (req, res) => {
if (req.user) {
res.success({ username: req.user.get('username') });
} else {
res.status(401).error('Not authenticated');
}
});
const result = await Parse.Cloud.run('expressWithUser', {});
expect(result.username).toEqual('testuser');
await Parse.User.logOut();
});
it('should support setting custom headers with res.header()', async () => {
Parse.Cloud.define('customHeaderFunction', (req, res) => {
res.header('X-Custom-Header', 'custom-value').success({ message: 'OK' });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/customHeaderFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(200);
expect(response.headers['x-custom-header']).toBe('custom-value');
expect(response.data.result.message).toBe('OK');
});
it('should support setting multiple custom headers', async () => {
Parse.Cloud.define('multipleHeadersFunction', (req, res) => {
res.header('X-Header-One', 'value1')
.header('X-Header-Two', 'value2')
.success({ message: 'Multiple headers' });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/multipleHeadersFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(200);
expect(response.headers['x-header-one']).toBe('value1');
expect(response.headers['x-header-two']).toBe('value2');
expect(response.data.result.message).toBe('Multiple headers');
});
it('should support combining status code and custom headers', async () => {
Parse.Cloud.define('statusAndHeaderFunction', (req, res) => {
res.status(201)
.header('X-Resource-Id', '12345')
.success({ created: true });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/statusAndHeaderFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(201);
expect(response.headers['x-resource-id']).toBe('12345');
expect(response.data.result.created).toBe(true);
});
});
});

View File

@@ -288,7 +288,15 @@ describe('Email Verification Token Expiration:', () => {
};
const verifyUserEmails = {
method(req) {
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
expect(Object.keys(req)).toEqual([
'original',
'object',
'master',
'ip',
'installationId',
'createdWith',
]);
expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
return false;
},
};
@@ -349,7 +357,15 @@ describe('Email Verification Token Expiration:', () => {
};
const verifyUserEmails = {
method(req) {
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
expect(Object.keys(req)).toEqual([
'original',
'object',
'master',
'ip',
'installationId',
'createdWith',
]);
expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
if (req.object.get('username') === 'no_email') {
return false;
}
@@ -384,6 +400,144 @@ describe('Email Verification Token Expiration:', () => {
expect(verifySpy).toHaveBeenCalledTimes(5);
});
it('provides createdWith on signup when verification blocks session creation', async () => {
const verifyUserEmails = {
method: params => {
expect(params.object).toBeInstanceOf(Parse.User);
expect(params.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
return true;
},
};
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: true,
preventSignupWithUnverifiedEmail: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
publicServerURL: 'http://localhost:8378/1',
});
const user = new Parse.User();
user.setUsername('signup_created_with');
user.setPassword('pass');
user.setEmail('signup@example.com');
const res = await user.signUp().catch(e => e);
expect(res.message).toBe('User email is not verified.');
expect(user.getSessionToken()).toBeUndefined();
expect(verifySpy).toHaveBeenCalledTimes(2); // before signup completion and on preventLoginWithUnverifiedEmail
});
it('provides createdWith with auth provider on login verification', async () => {
const user = new Parse.User();
user.setUsername('user_created_with_login');
user.setPassword('pass');
user.set('email', 'login@example.com');
await user.signUp();
const verifyUserEmails = {
method: async params => {
expect(params.object).toBeInstanceOf(Parse.User);
expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
return true;
},
};
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
publicServerURL: 'http://localhost:8378/1',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
preventSignupWithUnverifiedEmail: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
});
const res = await Parse.User.logIn('user_created_with_login', 'pass').catch(e => e);
expect(res.code).toBe(205);
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2); // before login completion and on preventLoginWithUnverifiedEmail
});
it('provides createdWith with auth provider on signup verification', async () => {
const createdWithValues = [];
const verifyUserEmails = {
method: params => {
createdWithValues.push(params.createdWith);
return true;
},
};
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: true,
preventSignupWithUnverifiedEmail: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
publicServerURL: 'http://localhost:8378/1',
});
const provider = {
authData: { id: '8675309', access_token: 'jenny' },
shouldError: false,
authenticate(options) {
options.success(this, this.authData);
},
restoreAuthentication() {
return true;
},
getAuthType() {
return 'facebook';
},
deauthenticate() {},
};
Parse.User._registerAuthenticationProvider(provider);
const res = await Parse.User._logInWith('facebook').catch(e => e);
expect(res.message).toBe('User email is not verified.');
// Called once in createSessionTokenIfNeeded (no email set, so _validateEmail skips)
expect(verifySpy).toHaveBeenCalledTimes(1);
expect(createdWithValues[0]).toEqual({ action: 'signup', authProvider: 'facebook' });
});
it('provides createdWith for preventLoginWithUnverifiedEmail function', async () => {
const user = new Parse.User();
user.setUsername('user_prevent_login_fn');
user.setPassword('pass');
user.set('email', 'preventlogin@example.com');
await user.signUp();
const preventLoginCreatedWith = [];
await reconfigureServer({
appName: 'emailVerifyToken',
publicServerURL: 'http://localhost:8378/1',
verifyUserEmails: true,
preventLoginWithUnverifiedEmail: params => {
preventLoginCreatedWith.push(params.createdWith);
return true;
},
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
});
const res = await Parse.User.logIn('user_prevent_login_fn', 'pass').catch(e => e);
expect(res.code).toBe(205);
expect(preventLoginCreatedWith.length).toBe(1);
expect(preventLoginCreatedWith[0]).toEqual({ action: 'login', authProvider: 'password' });
});
it_id('d812de87-33d1-495e-a6e8-3485f6dc3589')(it)('can conditionally send user email verification', async () => {
const emailAdapter = {
sendVerificationEmail: () => {},
@@ -779,6 +933,7 @@ describe('Email Verification Token Expiration:', () => {
expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined();
expect(params.resendRequest).toBeTrue();
expect(params.createdWith).toBeUndefined();
return true;
},
};

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -1180,4 +1180,127 @@ 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({
appId: 'test',
appName: 'exampleAppname',
publicServerURL: 'http://localhost:8378/1',
pages: { enableRouter: true },
});
});
it('should escape XSS payloads in token parameter', async () => {
const xssPayload = '"><script>alert("XSS")</script>';
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?token=${encodeURIComponent(xssPayload)}&username=test&appId=test`,
});
expect(response.status).toBe(200);
expect(response.text).not.toContain('<script>alert("XSS")</script>');
expect(response.text).toContain('&quot;&gt;&lt;script&gt;');
});
it('should escape XSS in username parameter', async () => {
const xssUsername = '<img src=x onerror=alert(1)>';
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(xssUsername)}&appId=test`,
});
expect(response.status).toBe(200);
expect(response.text).not.toContain('<img src=x onerror=alert(1)>');
expect(response.text).toContain('&lt;img');
});
it('should escape XSS in locale parameter', async () => {
const xssLocale = '"><svg/onload=alert(1)>';
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?locale=${encodeURIComponent(xssLocale)}&appId=test`,
});
expect(response.status).toBe(200);
expect(response.text).not.toContain('<svg/onload=alert(1)>');
expect(response.text).toContain('&quot;&gt;&lt;svg');
});
it('should handle legitimate usernames with quotes correctly', async () => {
const username = "O'Brien";
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(username)}&appId=test`,
});
expect(response.status).toBe(200);
// Should be properly escaped as HTML entity
expect(response.text).toContain('O&#39;Brien');
// Should NOT contain unescaped quote that breaks HTML
expect(response.text).not.toContain('value="O\'Brien"');
});
it('should handle legitimate usernames with ampersands correctly', async () => {
const username = 'Smith & Co';
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(username)}&appId=test`,
});
expect(response.status).toBe(200);
// Should be properly escaped
expect(response.text).toContain('Smith &amp; Co');
});
});
});

View File

@@ -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();
});

View File

@@ -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,
});
});

View File

@@ -284,6 +284,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
expect(params.ip).toBeDefined();
expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined();
expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
return true;
},
};

View File

@@ -133,6 +133,72 @@ describe('buildConfigDefinitions', () => {
expect(result.property.name).toBe('arrayParser');
});
it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (nullable)', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
typeAnnotation: {
types: [
{ type: 'BooleanTypeAnnotation' },
{ type: 'FunctionTypeAnnotation' },
],
},
};
const result = mapperFor(mockElement, t);
expect(t.isMemberExpression(result)).toBe(true);
expect(result.object.name).toBe('parsers');
expect(result.property.name).toBe('booleanOrFunctionParser');
});
it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (non-nullable)', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
types: [
{ type: 'BooleanTypeAnnotation' },
{ type: 'FunctionTypeAnnotation' },
],
};
const result = mapperFor(mockElement, t);
expect(t.isMemberExpression(result)).toBe(true);
expect(result.object.name).toBe('parsers');
expect(result.property.name).toBe('booleanOrFunctionParser');
});
it('should return undefined for UnionTypeAnnotation without boolean', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
typeAnnotation: {
types: [
{ type: 'StringTypeAnnotation' },
{ type: 'NumberTypeAnnotation' },
],
},
};
const result = mapperFor(mockElement, t);
expect(result).toBeUndefined();
});
it('should return undefined for UnionTypeAnnotation with boolean but without function', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
typeAnnotation: {
types: [
{ type: 'BooleanTypeAnnotation' },
{ type: 'VoidTypeAnnotation' },
],
},
};
const result = mapperFor(mockElement, t);
expect(result).toBeUndefined();
});
it('should return objectParser for unknown GenericTypeAnnotation', () => {
const mockElement = {
type: 'GenericTypeAnnotation',

View File

@@ -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();
});

View File

@@ -3,6 +3,7 @@ const {
numberOrBoolParser,
numberOrStringParser,
booleanParser,
booleanOrFunctionParser,
objectParser,
arrayParser,
moduleOrObjectParser,
@@ -48,6 +49,23 @@ describe('parsers', () => {
expect(parser(2)).toEqual(false);
});
it('parses correctly with booleanOrFunctionParser', () => {
const parser = booleanOrFunctionParser;
// Preserves functions
const fn = () => true;
expect(parser(fn)).toBe(fn);
const asyncFn = async () => false;
expect(parser(asyncFn)).toBe(asyncFn);
// Parses booleans and string booleans like booleanParser
expect(parser(true)).toEqual(true);
expect(parser(false)).toEqual(false);
expect(parser('true')).toEqual(true);
expect(parser('false')).toEqual(false);
expect(parser('1')).toEqual(true);
expect(parser(1)).toEqual(true);
expect(parser(0)).toEqual(false);
});
it('parses correctly with objectParser', () => {
const parser = objectParser;
expect(parser({ hello: 'world' })).toEqual({ hello: 'world' });

View File

@@ -3842,6 +3842,7 @@ describe('schemas', () => {
});
it_id('cbd5d897-b938-43a4-8f5a-5d02dd2be9be')(it_exclude_dbs(['postgres']))('cannot update to duplicate value on unique index', done => {
loggerErrorSpy.calls.reset();
const index = {
code: 1,
};
@@ -3868,6 +3869,12 @@ describe('schemas', () => {
.then(done.fail)
.catch(error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
// Client should only see generic message (no schema info exposed)
expect(error.message).toEqual('A duplicate value for a field with unique values was provided');
// Server logs should contain full MongoDB error message with detailed information
expect(loggerErrorSpy).toHaveBeenCalledWith('Duplicate key error:', jasmine.stringContaining('E11000 duplicate key error'));
expect(loggerErrorSpy).toHaveBeenCalledWith('Duplicate key error:', jasmine.stringContaining('test_UniqueIndexClass'));
expect(loggerErrorSpy).toHaveBeenCalledWith('Duplicate key error:', jasmine.stringContaining('code_1'));
done();
});
});

View File

@@ -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);

View File

@@ -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 });
}

View File

@@ -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);

View File

@@ -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', () => {});

View File

@@ -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;
}
@@ -519,7 +568,7 @@ export class MongoStorageAdapter implements StorageAdapter {
.then(() => ({ ops: [mongoObject] }))
.catch(error => {
if (error.code === 11000) {
// Duplicate value
logger.error('Duplicate key error:', error.message);
const err = new Parse.Error(
Parse.Error.DUPLICATE_VALUE,
'A duplicate value for a field with unique values was provided'
@@ -605,6 +654,7 @@ export class MongoStorageAdapter implements StorageAdapter {
.then(result => mongoObjectToParseObject(className, result, schema))
.catch(error => {
if (error.code === 11000) {
logger.error('Duplicate key error:', error.message);
throw new Parse.Error(
Parse.Error.DUPLICATE_VALUE,
'A duplicate value for a field with unique values was provided'

View File

@@ -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;
}
});

View File

@@ -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 });
}

View File

@@ -473,8 +473,8 @@ module.exports.ParseServerOptions = {
preventLoginWithUnverifiedEmail: {
env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL',
help:
'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`.<br>Requires option `verifyUserEmails: true`.',
action: parsers.booleanParser,
"Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.<br><br>The `createdWith` values per scenario:<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li></ul>Default is `false`.<br>Requires option `verifyUserEmails: true`.",
action: parsers.booleanOrFunctionParser,
default: false,
},
preventSignupWithUnverifiedEmail: {
@@ -574,6 +574,7 @@ module.exports.ParseServerOptions = {
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>',
action: parsers.booleanOrFunctionParser,
default: true,
},
serverCloseComplete: {
@@ -630,7 +631,8 @@ 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. Supports a function with a return value of `true` or `false` for conditional verification.<br><br>Default is `false`.',
"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. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.<br><br>The `createdWith` values per scenario:<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li><li>Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those</li></ul>Default is `false`.",
action: parsers.booleanOrFunctionParser,
default: false,
},
webhookKey: {
@@ -1179,6 +1181,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 +1470,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.',

View File

@@ -84,7 +84,7 @@
* @property {String} playgroundPath Mount path for the GraphQL Playground, defaults to /playground
* @property {Number} port The port to run the ParseServer, defaults to 1337.
* @property {Boolean} preserveFileName Enable (or disable) the addition of a unique hash to the file names
* @property {Boolean} preventLoginWithUnverifiedEmail 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`.<br>Requires option `verifyUserEmails: true`.
* @property {Boolean} preventLoginWithUnverifiedEmail Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.<br><br>The `createdWith` values per scenario:<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li></ul>Default is `false`.<br>Requires option `verifyUserEmails: true`.
* @property {Boolean} preventSignupWithUnverifiedEmail If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.<br><br>Default is `false`.<br>Requires option `verifyUserEmails: true`.
* @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details.
* @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.
@@ -108,7 +108,7 @@
* @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} verifyServerUrl Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.<br><br>⚠️ Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.<br><br>Default is `true`.
* @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 {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. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.<br><br>The `createdWith` values per scenario:<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li><li>Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those</li></ul>Default is `false`.
* @property {String} webhookKey Key sent with outgoing webhook calls
*/
@@ -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.

View File

@@ -43,6 +43,22 @@ type RequestKeywordDenylist = {
key: string | any,
value: any,
};
type EmailVerificationRequest = {
original?: any,
object: any,
master?: boolean,
ip?: string,
installationId?: string,
createdWith?: {
action: 'login' | 'signup',
authProvider: string,
},
resendRequest?: boolean,
};
type SendEmailVerificationRequest = {
user: any,
master?: boolean,
};
export interface ParseServerOptions {
/* Your Parse Application ID
@@ -174,18 +190,25 @@ 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. Supports a function with a return value of `true` or `false` for conditional verification.
/* 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. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.
<br><br>
The `createdWith` values per scenario:
<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li><li>Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those</li></ul>
Default is `false`.
:DEFAULT: false */
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.
verifyUserEmails: ?(boolean | (EmailVerificationRequest => boolean | Promise<boolean>));
/* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.
<br><br>
The `createdWith` values per scenario:
<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li></ul>
Default is `false`.
<br>
Requires option `verifyUserEmails: true`.
:DEFAULT: false */
preventLoginWithUnverifiedEmail: ?boolean;
preventLoginWithUnverifiedEmail: ?(
| boolean
| (EmailVerificationRequest => boolean | Promise<boolean>)
);
/* If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.
<br><br>
Default is `false`.
@@ -214,7 +237,10 @@ export interface ParseServerOptions {
Default is `true`.
<br>
:DEFAULT: true */
sendUserEmailVerification: ?(boolean | void);
sendUserEmailVerification: ?(
| boolean
| (SendEmailVerificationRequest => boolean | Promise<boolean>)
);
/* The account lockout policy for failed login attempts. */
accountLockout: ?AccountLockoutOptions;
/* The password policy for enforcing password related rules. */
@@ -755,6 +781,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 {

View File

@@ -68,6 +68,13 @@ function booleanParser(opt) {
return false;
}
function booleanOrFunctionParser(opt) {
if (typeof opt === 'function') {
return opt;
}
return booleanParser(opt);
}
function nullParser(opt) {
if (opt == 'null') {
return null;
@@ -81,6 +88,7 @@ module.exports = {
numberOrStringParser,
nullParser,
booleanParser,
booleanOrFunctionParser,
moduleOrObjectParser,
arrayParser,
objectParser,

View File

@@ -771,6 +771,30 @@ RestWrite.prototype._validateUserName = function () {
});
};
RestWrite.buildCreatedWith = function (action, authProvider) {
return { action, authProvider: authProvider || 'password' };
};
RestWrite.prototype.getCreatedWith = function () {
if (this.storage.createdWith) {
return this.storage.createdWith;
}
const isCreateOperation = !this.query;
const authDataProvider =
this.data?.authData &&
Object.keys(this.data.authData).length &&
Object.keys(this.data.authData).join(',');
const authProvider = this.storage.authProvider || authDataProvider;
// storage.authProvider is only set for login (existing user found in handleAuthData)
const action = this.storage.authProvider ? 'login' : isCreateOperation ? 'signup' : undefined;
if (!action) {
return;
}
const resolvedAuthProvider = authProvider || (action === 'signup' ? 'password' : undefined);
this.storage.createdWith = RestWrite.buildCreatedWith(action, resolvedAuthProvider);
return this.storage.createdWith;
};
/*
As with usernames, Parse should not allow case insensitive collisions of email.
unlike with usernames (which can have case insensitive collisions in the case of
@@ -826,6 +850,7 @@ RestWrite.prototype._validateEmail = function () {
master: this.auth.isMaster,
ip: this.config.ip,
installationId: this.auth.installationId,
createdWith: this.getCreatedWith(),
};
return this.config.userController.setEmailVerifyToken(this.data, request, this.storage);
}
@@ -961,6 +986,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () {
master: this.auth.isMaster,
ip: this.config.ip,
installationId: this.auth.installationId,
createdWith: this.getCreatedWith(),
};
// Get verification conditions which can be booleans or functions; the purpose of this async/await
// structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the
@@ -985,14 +1011,14 @@ RestWrite.prototype.createSessionToken = async function () {
if (this.storage.authProvider == null && this.data.authData) {
this.storage.authProvider = Object.keys(this.data.authData).join(',');
// Invalidate cached createdWith since authProvider was just resolved
delete this.storage.createdWith;
}
const createdWith = this.getCreatedWith();
const { sessionData, createSession } = RestWrite.createSession(this.config, {
userId: this.objectId(),
createdWith: {
action: this.storage.authProvider ? 'login' : 'signup',
authProvider: this.storage.authProvider || 'password',
},
createdWith,
installationId: this.auth.installationId,
});

View File

@@ -103,20 +103,52 @@ export class FunctionsRouter extends PromiseRouter {
});
}
static createResponseObject(resolve, reject) {
return {
static createResponseObject(resolve, reject, statusCode = null) {
let httpStatusCode = statusCode;
const customHeaders = {};
let responseSent = false;
const responseObject = {
success: function (result) {
resolve({
if (responseSent) {
throw new Error('Cannot call success() after response has already been sent. Make sure to call success() or error() only once per cloud function execution.');
}
responseSent = true;
const response = {
response: {
result: Parse._encode(result),
},
});
};
if (httpStatusCode !== null) {
response.status = httpStatusCode;
}
if (Object.keys(customHeaders).length > 0) {
response.headers = customHeaders;
}
resolve(response);
},
error: function (message) {
if (responseSent) {
throw new Error('Cannot call error() after response has already been sent. Make sure to call success() or error() only once per cloud function execution.');
}
responseSent = true;
const error = triggers.resolveError(message);
// If a custom status code was set, attach it to the error
if (httpStatusCode !== null) {
error.status = httpStatusCode;
}
reject(error);
},
status: function (code) {
httpStatusCode = code;
return responseObject;
},
header: function (key, value) {
customHeaders[key] = value;
return responseObject;
},
_isResponseSent: () => responseSent,
};
return responseObject;
}
static handleCloudFunction(req) {
const functionName = req.params.functionName;
@@ -143,7 +175,7 @@ export class FunctionsRouter extends PromiseRouter {
return new Promise(function (resolve, reject) {
const userString = req.auth && req.auth.user ? req.auth.user.id : undefined;
const { success, error } = FunctionsRouter.createResponseObject(
const responseObject = FunctionsRouter.createResponseObject(
result => {
try {
if (req.config.logLevels.cloudFunctionSuccess !== 'silent') {
@@ -184,14 +216,37 @@ export class FunctionsRouter extends PromiseRouter {
}
}
);
const { success, error } = responseObject;
return Promise.resolve()
.then(() => {
return triggers.maybeRunValidator(request, functionName, req.auth);
})
.then(() => {
return theFunction(request);
// Check if function expects 2 parameters (req, res) - Express style
if (theFunction.length >= 2) {
return theFunction(request, responseObject);
} else {
// Traditional style - single parameter
return theFunction(request);
}
})
.then(success, error);
.then(result => {
// For Express-style functions, only send response if not already sent
if (theFunction.length >= 2) {
if (!responseObject._isResponseSent()) {
// If Express-style function returns a value without calling res.success/error
if (result !== undefined) {
success(result);
}
// If no response sent and no value returned, this is an error in user code
// but we don't handle it here to maintain backward compatibility
}
} else {
// For traditional functions, always call success with the result (even if undefined)
success(result);
}
}, error);
});
}
}

View File

@@ -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);

View File

@@ -140,11 +140,17 @@ export class UsersRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
}
// Create request object for verification functions
const authProvider =
req.body &&
req.body.authData &&
Object.keys(req.body.authData).length &&
Object.keys(req.body.authData).join(',');
const request = {
master: req.auth.isMaster,
ip: req.config.ip,
installationId: req.auth.installationId,
object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)),
createdWith: RestWrite.buildCreatedWith('login', authProvider),
};
// If request doesn't use master or maintenance key with ignoring email verification
@@ -290,10 +296,7 @@ export class UsersRouter extends ClassesRouter {
const { sessionData, createSession } = RestWrite.createSession(req.config, {
userId: user.objectId,
createdWith: {
action: 'login',
authProvider: 'password',
},
createdWith: RestWrite.buildCreatedWith('login'),
installationId: req.info.installationId,
});
@@ -360,10 +363,7 @@ export class UsersRouter extends ClassesRouter {
const { sessionData, createSession } = RestWrite.createSession(req.config, {
userId,
createdWith: {
action: 'login',
authProvider: 'masterkey',
},
createdWith: RestWrite.buildCreatedWith('login', 'masterkey'),
installationId: req.info.installationId,
});

View File

@@ -107,22 +107,49 @@ var ParseCloud = {};
*
* **Available in Cloud Code only.**
*
* **Traditional Style:**
* ```
* Parse.Cloud.define('functionName', (request) => {
* // code here
* return result;
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.define('functionName', (request) => {
* // code here
* return result;
* }, { ...validationObject });
* ```
*
* **Express Style with Custom HTTP Status Codes:**
* ```
* Parse.Cloud.define('functionName', (request, response) => {
* // Set custom HTTP status code and send response
* response.status(201).success({ message: 'Created' });
* });
*
* Parse.Cloud.define('unauthorizedFunction', (request, response) => {
* if (!request.user) {
* response.status(401).error('Unauthorized');
* } else {
* response.success({ data: 'OK' });
* }
* });
*
* Parse.Cloud.define('withCustomHeaders', (request, response) => {
* response.header('X-Custom-Header', 'value').success({ data: 'OK' });
* });
*
* Parse.Cloud.define('errorFunction', (request, response) => {
* response.error('Something went wrong');
* });
* ```
*
* @static
* @memberof Parse.Cloud
* @param {String} name The name of the Cloud Function
* @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}.
* @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or two parameters (request, response) for Express-style functions where response is a {@link Parse.Cloud.FunctionResponse}.
* @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.define = function (functionName, handler, validationHandler) {
@@ -788,9 +815,22 @@ module.exports = ParseCloud;
* @property {Boolean} master If true, means the master key was used.
* @property {Parse.User} user If set, the user that made the request.
* @property {Object} params The params passed to the cloud function.
* @property {String} ip The IP address of the client making the request.
* @property {Object} headers The original HTTP headers for the request.
* @property {Object} log The current logger inside Parse Server.
* @property {String} functionName The name of the cloud function.
* @property {Object} context The context of the cloud function call.
* @property {Object} config The Parse Server config.
*/
/**
* @interface Parse.Cloud.FunctionResponse
* @property {function} success Call this function to return a successful response with an optional result. Usage: `response.success(result)`
* @property {function} error Call this function to return an error response with an error message. Usage: `response.error(message)`
* @property {function} status Call this function to set a custom HTTP status code for the response. Returns the response object for chaining. Usage: `response.status(code).success(result)` or `response.status(code).error(message)`
* @property {function} header Call this function to set a custom HTTP header for the response. Returns the response object for chaining. Usage: `response.header('X-Custom-Header', 'value').success(result)`
*/
/**
* @interface Parse.Cloud.JobRequest
* @property {Object} params The params passed to the background job.

View File

@@ -38,6 +38,7 @@ export const DefaultMongoURI = DefinitionDefaults.databaseURI;
// before passing to MongoDB client
export const ParseServerDatabaseOptions = [
'allowPublicExplain',
'clientMetadata',
'createIndexRoleName',
'createIndexUserEmail',
'createIndexUserEmailCaseInsensitive',

View File

@@ -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'));
}
};

View File

@@ -26,6 +26,22 @@ type RequestKeywordDenylist = {
key: string;
value: any;
};
export interface EmailVerificationRequest {
original?: any;
object: any;
master?: boolean;
ip?: string;
installationId?: string;
createdWith?: {
action: 'login' | 'signup';
authProvider: string;
};
resendRequest?: boolean;
}
export interface SendEmailVerificationRequest {
user: any;
master?: boolean;
}
export interface ParseServerOptions {
appId: string;
masterKey: (() => void) | string;
@@ -74,12 +90,12 @@ export interface ParseServerOptions {
auth?: Record<string, AuthAdapter>;
enableInsecureAuthAdapters?: boolean;
maxUploadSize?: string;
verifyUserEmails?: (boolean | void);
preventLoginWithUnverifiedEmail?: boolean;
verifyUserEmails?: boolean | ((params: EmailVerificationRequest) => boolean | Promise<boolean>);
preventLoginWithUnverifiedEmail?: boolean | ((params: EmailVerificationRequest) => boolean | Promise<boolean>);
preventSignupWithUnverifiedEmail?: boolean;
emailVerifyTokenValidityDuration?: number;
emailVerifyTokenReuseIfValid?: boolean;
sendUserEmailVerification?: (boolean | void);
sendUserEmailVerification?: boolean | ((params: SendEmailVerificationRequest) => boolean | Promise<boolean>);
accountLockout?: AccountLockoutOptions;
passwordPolicy?: PasswordPolicyOptions;
cacheAdapter?: Adapter<CacheAdapter>;