mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
286
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1979de03da | ||
|
|
6f2fc443c8 | ||
|
|
c696e1e044 | ||
|
|
70146bbdce | ||
|
|
694da02ce7 | ||
|
|
e1d4d122b6 | ||
|
|
68922c89bc | ||
|
|
1e21b69376 | ||
|
|
3353998edd | ||
|
|
cf4c8807b9 | ||
|
|
892052cc44 | ||
|
|
a6e0a1d4a6 | ||
|
|
28c4330c89 | ||
|
|
c764203ce8 | ||
|
|
ef1634bf1f | ||
|
|
e7199e8d07 | ||
|
|
2ecc5a5bf5 | ||
|
|
cd61c937f4 | ||
|
|
dca187f91b | ||
|
|
6bdd87c21a | ||
|
|
284da09f45 | ||
|
|
4906644d67 | ||
|
|
0633f98562 | ||
|
|
31ad5d124e | ||
|
|
91dde99d3d | ||
|
|
b6452633a7 | ||
|
|
4116f571a7 | ||
|
|
39e529d458 | ||
|
|
2edf1e4c03 | ||
|
|
ae72cf08bf | ||
|
|
665b8d52d6 | ||
|
|
885aa12c8c | ||
|
|
c71deef865 | ||
|
|
cb73e2d94b | ||
|
|
5000c2ac3b | ||
|
|
73eec3dfb1 | ||
|
|
480282cf18 | ||
|
|
e6be5117d0 | ||
|
|
5379135c88 | ||
|
|
42ff468960 | ||
|
|
162c07b0a7 | ||
|
|
11d3e484df | ||
|
|
716925ec53 | ||
|
|
18f2002b84 | ||
|
|
61841859cc | ||
|
|
61653a3983 | ||
|
|
0a054e6b54 | ||
|
|
d8ebdb3632 | ||
|
|
ca20496f28 | ||
|
|
b4739509f7 | ||
|
|
9d0bd2badd | ||
|
|
eba9dfff7b | ||
|
|
242002432f | ||
|
|
5b0efb22ef | ||
|
|
6364948d81 | ||
|
|
e01c44a15b | ||
|
|
bceb621a32 | ||
|
|
2532acfce8 | ||
|
|
2c21aed890 | ||
|
|
8758e6abb9 | ||
|
|
f1469c6425 | ||
|
|
855b5df758 | ||
|
|
480fc57984 | ||
|
|
390bb92c15 | ||
|
|
45d8ff9802 | ||
|
|
9ac4fa70c9 | ||
|
|
18bdbf89c5 | ||
|
|
e31d230026 | ||
|
|
33c06b261e | ||
|
|
d58d159fa2 | ||
|
|
9f6da23511 | ||
|
|
a45d34691c | ||
|
|
3a087e7ad8 | ||
|
|
7959083760 | ||
|
|
06e54f1d37 | ||
|
|
4420835afa | ||
|
|
c277739623 | ||
|
|
cfcb943ba5 | ||
|
|
b07ec15382 | ||
|
|
79846f7c15 | ||
|
|
dc48be85f7 | ||
|
|
de54d1e9bc | ||
|
|
e5b5c444b0 | ||
|
|
1c0eced4bd | ||
|
|
854ac9a017 | ||
|
|
5f81efb429 | ||
|
|
7d84b244ab | ||
|
|
907ad4267c | ||
|
|
6fb19bd83a | ||
|
|
da368ea763 | ||
|
|
6d4663b47e | ||
|
|
1a2b513e8c | ||
|
|
c83de8c4ea | ||
|
|
4c74b2b926 | ||
|
|
60cf2dd054 | ||
|
|
0d5acf3d9b | ||
|
|
5c2c120021 | ||
|
|
63d4880ebf | ||
|
|
09310a7700 | ||
|
|
9f6e3429d3 | ||
|
|
901aaf8cd3 | ||
|
|
ad4aa83983 | ||
|
|
1c2bb7ca28 | ||
|
|
ba098970df | ||
|
|
1ffc48fd08 | ||
|
|
720d24e185 | ||
|
|
2065897dc6 | ||
|
|
37e3934a85 | ||
|
|
6f20e54cce | ||
|
|
a5050df7fe | ||
|
|
90e103528d | ||
|
|
c7b2068329 | ||
|
|
2760381183 | ||
|
|
3fbd926b80 | ||
|
|
41f009db4e | ||
|
|
fbf6d41a00 | ||
|
|
a56fb76433 | ||
|
|
98edbdbd4b | ||
|
|
3de9acc006 | ||
|
|
cf99570c75 | ||
|
|
21458ff502 | ||
|
|
7773d3b5ef | ||
|
|
d3f0886707 | ||
|
|
2988c450e1 | ||
|
|
042d69614e | ||
|
|
edd416b647 | ||
|
|
29624e0fae | ||
|
|
fe1e4d9775 | ||
|
|
1f62dd0f4e | ||
|
|
f9c39edcbc | ||
|
|
2170962a50 | ||
|
|
afcafdba1e | ||
|
|
a84ecc9fec | ||
|
|
9d3030f6a8 | ||
|
|
dfea561f44 | ||
|
|
0613174b34 | ||
|
|
dbe8b72d8a | ||
|
|
9293ce8ca5 | ||
|
|
105ae7c8a5 | ||
|
|
01c97f7ab7 | ||
|
|
a7b5b38418 | ||
|
|
5452c8f41f | ||
|
|
cbefe770a7 | ||
|
|
9c85e63354 | ||
|
|
ab58eee0de | ||
|
|
ec5a8ccbfd | ||
|
|
763738c92b | ||
|
|
6579e8254c | ||
|
|
f5d6fc98e7 | ||
|
|
68b745a604 | ||
|
|
94846cd4de | ||
|
|
1d80aa25e3 | ||
|
|
597a9c3163 | ||
|
|
1f6dadbb81 | ||
|
|
ac7399b226 | ||
|
|
9001df5063 | ||
|
|
a6d574ede6 | ||
|
|
70c280ca57 | ||
|
|
e73bc517a8 | ||
|
|
25796537f4 | ||
|
|
efc5897f0b | ||
|
|
5202212b0b | ||
|
|
c604657a27 | ||
|
|
514c2377e0 | ||
|
|
dfd1aee7dd | ||
|
|
af686f31da | ||
|
|
0cf58eb8d6 | ||
|
|
0e9b6d69ef | ||
|
|
e29845f8da | ||
|
|
bbda8d2ed6 | ||
|
|
38983e8e9b | ||
|
|
359b66f9d1 | ||
|
|
4e6a375b51 | ||
|
|
df3d0bc82d | ||
|
|
1aba6382c8 | ||
|
|
6f21195980 | ||
|
|
a2ac8d133c | ||
|
|
27ad6f476a | ||
|
|
ced487246e | ||
|
|
104990b52a | ||
|
|
99489b22e4 | ||
|
|
655e2c41f0 | ||
|
|
633a9d25e4 | ||
|
|
abdba68380 | ||
|
|
66e36039d8 | ||
|
|
038b7a978b | ||
|
|
1eb95aeb41 | ||
|
|
355baf9dfc | ||
|
|
e315c137bf | ||
|
|
3c07fcada6 | ||
|
|
0023ce448a | ||
|
|
4aba66c620 | ||
|
|
8adcbee112 | ||
|
|
0d58e3972c | ||
|
|
3b9f50ac90 | ||
|
|
972f630016 | ||
|
|
19fc5461d5 | ||
|
|
69c927c907 | ||
|
|
8e7a6b1480 | ||
|
|
f9dde4a9f8 | ||
|
|
a22f095a47 | ||
|
|
a22dbe16d5 | ||
|
|
2c2563ebd2 | ||
|
|
3de8494a22 | ||
|
|
d3087ed69f | ||
|
|
b87daba067 | ||
|
|
759731926f | ||
|
|
0762ba6b79 | ||
|
|
6ef1986c03 | ||
|
|
988ecdac54 | ||
|
|
87059ae1d6 | ||
|
|
42e2e12956 | ||
|
|
d58032d39d | ||
|
|
5b1bb598e4 | ||
|
|
2612a387ce | ||
|
|
c6355cda73 | ||
|
|
90aac622b0 | ||
|
|
4b3ce20300 | ||
|
|
09fbeebba8 | ||
|
|
80b987d00d | ||
|
|
9e0094980f | ||
|
|
f630a45aa5 | ||
|
|
ea57a7706d | ||
|
|
fe02d3e8aa | ||
|
|
b0c012e835 | ||
|
|
fd86278919 | ||
|
|
5dd3aa0d48 | ||
|
|
2d6b3d1849 | ||
|
|
93af48a8b4 | ||
|
|
5462834240 | ||
|
|
7d32d8934f | ||
|
|
8d3117e0bc | ||
|
|
a2a98b1684 | ||
|
|
77bbfb3f18 | ||
|
|
9b9c3a4214 | ||
|
|
3d6d50e0af | ||
|
|
b70c2d9027 | ||
|
|
71dfd8a7ec | ||
|
|
39a91d0d77 | ||
|
|
4945ab7520 | ||
|
|
a9c34ef1e2 | ||
|
|
05939858af | ||
|
|
f5e20f9121 | ||
|
|
9c6cdf4904 | ||
|
|
4baeae4f4f | ||
|
|
3602ecb169 | ||
|
|
7f89399300 | ||
|
|
88a9106956 | ||
|
|
391c7a02aa | ||
|
|
6ea65f255f | ||
|
|
45a3ed0fcf | ||
|
|
977edeaf28 | ||
|
|
739ffbed86 | ||
|
|
5954f0ffa0 | ||
|
|
d141b822ad | ||
|
|
be4c7e23c6 | ||
|
|
877eede075 | ||
|
|
2b3d4e5d3c | ||
|
|
42929e0e5e | ||
|
|
b9bdca4520 | ||
|
|
ae68f0c31b | ||
|
|
7a198f4eed | ||
|
|
95da5d64c1 | ||
|
|
6a4a00ca7a | ||
|
|
c9b59719ec | ||
|
|
02f40fd896 | ||
|
|
a9d376b61f | ||
|
|
a742656a90 | ||
|
|
4e2000bc56 | ||
|
|
446cbb1a31 | ||
|
|
3f03bd3c6f | ||
|
|
31805c96ec | ||
|
|
d6b17baa32 | ||
|
|
328918178f | ||
|
|
3dd99dd80e | ||
|
|
f8b5a99d54 | ||
|
|
cc079a40f6 | ||
|
|
3ec3e40dc8 | ||
|
|
068fb9e777 | ||
|
|
e212eb5195 | ||
|
|
1850be45b1 | ||
|
|
e6bd2baf71 | ||
|
|
44acd6d9ed | ||
|
|
3710da7379 | ||
|
|
9674d4a2c0 | ||
|
|
45301a67e8 |
+5
-1
@@ -24,6 +24,10 @@
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off"
|
||||
"require-atomic-updates": "off",
|
||||
"object-curly-spacing": ["error", "always"]
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,10 @@ assignees: ''
|
||||
---
|
||||
|
||||
### New Issue Checklist
|
||||
<!--
|
||||
Check every following box [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Platform!
|
||||
-->
|
||||
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
|
||||
- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- [ ] I can reproduce the issue with the [latest version of Parse Server](https://github.com/parse-community/parse-server/releases). <!-- We don't investigate issues for outdated releases. -->
|
||||
- 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).
|
||||
- Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
### Issue Description
|
||||
<!-- What is the specific issue with Parse Server? -->
|
||||
@@ -30,6 +24,7 @@ assignees: ''
|
||||
|
||||
### Expected Outcome
|
||||
<!-- What outcome, for example query result, did you expect? -->
|
||||
|
||||
### Environment
|
||||
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->
|
||||
|
||||
|
||||
@@ -8,15 +8,10 @@ assignees: ''
|
||||
---
|
||||
|
||||
### New Feature / Enhancement Checklist
|
||||
<!--
|
||||
Check every following box [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Platform!
|
||||
-->
|
||||
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
|
||||
- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- 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).
|
||||
- Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
### Current Limitation
|
||||
<!-- Which current limitation is the feature or enhancement addressing? -->
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Dependabot dependency updates
|
||||
# Docs: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
# Location of package-lock.json
|
||||
directory: "/"
|
||||
# Check daily for updates
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
# Set commit message prefix
|
||||
prefix: "refactor"
|
||||
@@ -13,18 +13,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Environments Check
|
||||
@@ -36,7 +30,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::ci-bump-environment"
|
||||
|
||||
+123
-127
@@ -1,14 +1,14 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta, next-major, 'release-[0-9]+.x.x' ]
|
||||
branches: [release, alpha, beta, next-major, 'release-[0-9]+.x.x']
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
paths-ignore:
|
||||
- '**/**.md'
|
||||
env:
|
||||
NODE_VERSION: 19.3.0
|
||||
NODE_VERSION: 20.12.0
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
jobs:
|
||||
check-code-analysis:
|
||||
@@ -21,29 +21,45 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
language: ['javascript']
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
source-root: src
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
source-root: src
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
check-ci:
|
||||
name: Node Engine Check
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install prod dependencies
|
||||
run: npm ci
|
||||
- name: Remove dev dependencies
|
||||
run: ./ci/uninstallDevDeps.sh @actions/core
|
||||
- name: CI Node Engine Check
|
||||
run: npm run ci:checkNodeEngine
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -51,40 +67,19 @@ jobs:
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Node Engine Check
|
||||
run: npm run ci:checkNodeEngine
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run lint
|
||||
check-definitions:
|
||||
name: Check Definitions
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
name: Check Definitions
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -95,48 +90,48 @@ jobs:
|
||||
- name: CI Definitions Check
|
||||
run: npm run ci:definitionsCheck
|
||||
check-circular:
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run madge:circular
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run madge:circular
|
||||
check-docker:
|
||||
name: Docker Build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
check-lock-file-version:
|
||||
name: NPM Lock File Version
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check NPM lock file version
|
||||
uses: mansona/npm-lockfile-version@v1
|
||||
with:
|
||||
@@ -146,38 +141,34 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- name: MongoDB 4.2, ReplicaSet
|
||||
MONGODB_VERSION: 4.2.19
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
MONGODB_VERSION: 4.2.25
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: MongoDB 4.4, ReplicaSet
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
MONGODB_VERSION: 4.4.29
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: MongoDB 5, ReplicaSet
|
||||
MONGODB_VERSION: 5.3.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
MONGODB_VERSION: 5.0.26
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: MongoDB 6, ReplicaSet
|
||||
MONGODB_VERSION: 6.0.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
MONGODB_VERSION: 6.0.14
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: MongoDB 7, ReplicaSet
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: Redis Cache
|
||||
PARSE_SERVER_TEST_CACHE: redis
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: Node 14
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 14.21.1
|
||||
- name: Node 16
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 16.18.1
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: Node 18
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 18.12.1
|
||||
NODE_VERSION: 18.20.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
@@ -186,7 +177,7 @@ jobs:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
- 6379:6379
|
||||
env:
|
||||
MONGODB_VERSION: ${{ matrix.MONGODB_VERSION }}
|
||||
MONGODB_TOPOLOGY: ${{ matrix.MONGODB_TOPOLOGY }}
|
||||
@@ -196,16 +187,13 @@ jobs:
|
||||
steps:
|
||||
- name: Fix usage of insecure GitHub protocol
|
||||
run: sudo git config --system url."https://github".insteadOf "git://github"
|
||||
- name: Fix git protocol for Node 14
|
||||
if: ${{ startsWith(matrix.NODE_VERSION, '14.') }}
|
||||
run: sudo git config --system url."https://github".insteadOf "ssh://git@github"
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -217,35 +205,37 @@ jobs:
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
# Set to `true` once codecov token bug is fixed; https://github.com/parse-community/parse-server/issues/9129
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
check-postgres:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: PostgreSQL 11, PostGIS 3.0
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.0
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.1
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.2
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 12, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:12-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 13, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.1
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 13, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.2
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 13, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 14, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 15, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 13, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.4
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 14, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.4
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 15, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.4
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 16, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.4
|
||||
NODE_VERSION: 20.12.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
@@ -271,13 +261,13 @@ jobs:
|
||||
PARSE_SERVER_TEST_DATABASE_URI: postgres://postgres:postgres@localhost:5432/parse_server_postgres_adapter_test_database
|
||||
NODE_VERSION: ${{ matrix.NODE_VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -291,7 +281,13 @@ jobs:
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -12,15 +12,15 @@ jobs:
|
||||
- name: Determine trigger branch name
|
||||
id: branch
|
||||
run: echo "::set-output name=trigger_branch::${GITHUB_REF#refs/*/}"
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.1.0
|
||||
node-version: 20
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -51,23 +51,23 @@ jobs:
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.current_tag }}
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Log into Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
@@ -75,10 +75,10 @@ jobs:
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=${{ needs.release.outputs.current_tag }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm/v6, linux/arm/v7, linux/arm64/v8
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -89,13 +89,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.1.0
|
||||
node-version: 18.20.0
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -112,4 +112,4 @@ jobs:
|
||||
uses: peaceiris/actions-gh-pages@v3.7.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
publish_dir: ./docs
|
||||
|
||||
@@ -23,23 +23,23 @@ jobs:
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Log into Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
@@ -48,10 +48,10 @@ jobs:
|
||||
type=semver,enable=true,pattern={{version}},value=${{ github.event.inputs.ref }}
|
||||
type=raw,enable=${{ github.event.inputs.ref == '' }},value=latest
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm/v6, linux/arm/v7, linux/arm64/v8
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
* Semantic Release Config
|
||||
*/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { readFile } = require('fs').promises;
|
||||
const { resolve } = require('path');
|
||||
|
||||
// For ES6 modules use:
|
||||
// import { readFile } from 'fs/promises';
|
||||
// import { resolve, dirname } from 'path';
|
||||
// import { fileURLToPath } from 'url';
|
||||
|
||||
// Get env vars
|
||||
const ref = process.env.GITHUB_REF;
|
||||
@@ -24,7 +29,7 @@ const templates = {
|
||||
async function config() {
|
||||
|
||||
// Get branch
|
||||
const branch = ref.split('/').pop().split('-')[0];
|
||||
const branch = ref?.split('/')?.pop()?.split('-')[0] || '(current branch could not be determined)';
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
@@ -40,8 +45,8 @@ async function config() {
|
||||
{ name: 'alpha', prerelease: true },
|
||||
{ name: 'beta', prerelease: true },
|
||||
'next-major',
|
||||
// Long-Term-Support branches; defined as GLOB pattern
|
||||
'release-+([0-9]).x.x',
|
||||
// Long-Term-Support branch of previous major version
|
||||
'release-6.x.x',
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
@@ -89,7 +94,7 @@ async function config() {
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"branches": [
|
||||
"backmergeBranches": [
|
||||
{ from: "beta", to: "alpha" },
|
||||
{ from: "release", to: "beta" },
|
||||
]
|
||||
@@ -103,15 +108,17 @@ async function config() {
|
||||
|
||||
async function loadTemplates() {
|
||||
for (const template of Object.keys(templates)) {
|
||||
const text = await readFile(path.resolve(__dirname, resourcePath, templates[template].file));
|
||||
|
||||
// For ES6 modules use:
|
||||
// const fileUrl = import.meta.url;
|
||||
// const __dirname = dirname(fileURLToPath(fileUrl));
|
||||
|
||||
const filePath = resolve(__dirname, resourcePath, templates[template].file);
|
||||
const text = await readFile(filePath, 'utf-8');
|
||||
templates[template].text = text;
|
||||
}
|
||||
}
|
||||
|
||||
async function readFile(filePath) {
|
||||
return await fs.readFile(filePath, 'utf-8');
|
||||
}
|
||||
|
||||
function getReleaseComment() {
|
||||
const url = repositoryUrl + '/releases/tag/${nextRelease.gitTag}';
|
||||
const comment = '🎉 This change has been released in version [${nextRelease.version}](' + url + ')';
|
||||
+8
-4
@@ -242,7 +242,11 @@ Once you have babel running in watch mode, you can start making changes to parse
|
||||
|
||||
If your pull request introduces a change that may affect the storage or retrieval of objects, you may want to make sure it plays nice with Postgres.
|
||||
|
||||
* Run the tests against the postgres database with `PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly`. You'll need to have postgres running on your machine and setup [appropriately](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) or use [`Docker`](#run-a-parse-postgres-with-docker).
|
||||
* You'll need to have postgres running on your machine and setup [appropriately](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) or use [`Docker`](#postgres-with-docker)
|
||||
* Run the tests against the postgres database with:
|
||||
```
|
||||
PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly
|
||||
```
|
||||
* The Postgres adapter has a special debugger that traces all the sql commands. You can enable it with setting the environment variable `PARSE_SERVER_LOG_LEVEL=debug`
|
||||
* If your feature is intended to only work with MongoDB, you should disable PostgreSQL-specific tests with:
|
||||
|
||||
@@ -264,10 +268,10 @@ If your pull request introduces a change that may affect the storage or retrieva
|
||||
|
||||
#### Postgres with Docker
|
||||
|
||||
[PostGIS images (select one with v2.2 or higher) on docker dashboard](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://registry.hub.docker.com/_/postgres/) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
|
||||
[PostGIS images (select one with v2.2 or higher) on docker hub](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://hub.docker.com/_/postgres) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
|
||||
|
||||
```
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.2-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:16-3.4-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
```
|
||||
To stop the Postgres instance:
|
||||
|
||||
@@ -275,7 +279,7 @@ To stop the Postgres instance:
|
||||
docker stop parse-postgres
|
||||
```
|
||||
|
||||
You can also use the [postgis/postgis:13-3.2-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
You can also use the [postgis/postgis:16-3.4-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
|
||||
```
|
||||
#Install additional scripts. These are run in abc order during initial start
|
||||
|
||||
+6
-5
@@ -8,11 +8,12 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
|
||||
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS10 | Config option `encodeParseObjectInCloudFunction` defaults to `true` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 8.0.0 (2025) | deprecated | - |
|
||||
|
||||
[i_deprecation]: ## "The version and date of the deprecation."
|
||||
[i_removal]: ## "The version and date of the planned removal."
|
||||
|
||||
+7
-3
@@ -1,9 +1,13 @@
|
||||
############################################################
|
||||
# Build stage
|
||||
############################################################
|
||||
FROM node:lts-alpine AS build
|
||||
FROM node:20.14.0-alpine3.20 AS build
|
||||
|
||||
RUN apk --no-cache add \
|
||||
build-base \
|
||||
git \
|
||||
python3
|
||||
|
||||
RUN apk --no-cache add git
|
||||
WORKDIR /tmp
|
||||
|
||||
# Copy package.json first to benefit from layer caching
|
||||
@@ -24,7 +28,7 @@ RUN npm ci --omit=dev --ignore-scripts \
|
||||
############################################################
|
||||
# Release stage
|
||||
############################################################
|
||||
FROM node:lts-alpine AS release
|
||||
FROM node:20.14.0-alpine3.20 AS release
|
||||
|
||||
VOLUME /parse-server/cloud /parse-server/config
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Abeta)
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Arelease)
|
||||
[](https://snyk.io/test/github/parse-community/parse-server)
|
||||
[](https://codecov.io/github/parse-community/parse-server?branch=alpha)
|
||||
[](https://app.codecov.io/github/parse-community/parse-server/tree/alpha)
|
||||
[](https://github.com/parse-community/parse-dashboard/releases)
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
@@ -129,22 +129,20 @@ Parse Server is continuously tested with the most recent releases of Node.js to
|
||||
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|------------|----------------|-------------|------------|
|
||||
| Node.js 14 | 14.19.1 | April 2023 | ✅ Yes |
|
||||
| Node.js 16 | 16.14.2 | April 2024 | ✅ Yes |
|
||||
| Node.js 18 | 18.12.1 | April 2025 | ✅ Yes |
|
||||
| Node.js 19 | 19.3.0 | June 2023 | ✅ Yes |
|
||||
| Node.js 18 | 18.20.0 | April 2025 | ✅ Yes |
|
||||
| Node.js 20 | 20.12.0 | April 2026 | ✅ Yes |
|
||||
|
||||
#### MongoDB
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. We consider the end-of-life date of a MongoDB "rapid release" to be the same as its major version release.
|
||||
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. MongoDB "rapid releases" are ignored as these are considered pre-releases of the next major version.
|
||||
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|-------------|----------------|---------------|------------|
|
||||
| MongoDB 4.0 | 4.0.28 | April 2022 | ✅ Yes |
|
||||
| MongoDB 4.2 | 4.2.19 | April 2023 | ✅ Yes |
|
||||
| MongoDB 4.4 | 4.4.13 | February 2024 | ✅ Yes |
|
||||
| MongoDB 5 | 5.3.2 | October 2024 | ✅ Yes |
|
||||
| MongoDB 6 | 6.0.2 | July 2025 | ✅ Yes |
|
||||
| ----------- | -------------- | ------------- | ---------- |
|
||||
| MongoDB 4.2 | 4.2.25 | April 2023 | ✅ Yes |
|
||||
| MongoDB 4.4 | 4.4.29 | February 2024 | ✅ Yes |
|
||||
| MongoDB 5 | 5.0.26 | October 2024 | ✅ Yes |
|
||||
| MongoDB 6 | 6.0.14 | July 2025 | ✅ Yes |
|
||||
| MongoDB 7 | 7.0.8 | TDB | ✅ Yes |
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
@@ -152,11 +150,10 @@ Parse Server is continuously tested with the most recent releases of PostgreSQL
|
||||
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support | Compatible |
|
||||
|-------------|--------------------|---------------|----------------------|------------|
|
||||
| Postgres 11 | 3.0, 3.1, 3.2, 3.3 | November 2023 | <= 5.x (2022) | ✅ Yes |
|
||||
| Postgres 12 | 3.3 | November 2024 | <= 5.x (2022) | ✅ Yes |
|
||||
| Postgres 13 | 3.3 | November 2025 | <= 6.x (2023) | ✅ Yes |
|
||||
| Postgres 14 | 3.3 | November 2026 | <= 7.x (2024) | ✅ Yes |
|
||||
| Postgres 15 | 3.3 | November 2027 | <= 8.x (2025) | ✅ Yes |
|
||||
| Postgres 13 | 3.1, 3.2, 3.3, 3.4 | November 2025 | <= 6.x (2023) | ✅ Yes |
|
||||
| Postgres 14 | 3.4 | November 2026 | <= 7.x (2024) | ✅ Yes |
|
||||
| Postgres 15 | 3.4 | November 2027 | <= 8.x (2025) | ✅ Yes |
|
||||
| Postgres 16 | 3.4 | November 2028 | <= 9.x (2026) | ✅ Yes |
|
||||
|
||||
### Locally
|
||||
|
||||
@@ -358,12 +355,14 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
|
||||
## Access Scopes
|
||||
|
||||
| Scope | Internal data | Custom data | Restricted by CLP, ACL | Key |
|
||||
|----------------|---------------|-------------|------------------------|---------------------|
|
||||
| Internal | r/w | r/w | no | `maintenanceKey` |
|
||||
| Master | -/- | r/w | no | `masterKey` |
|
||||
| ReadOnlyMaster | -/- | r/- | no | `readOnlyMasterKey` |
|
||||
| Session | -/- | r/w | yes | `sessionToken` |
|
||||
| Scope | Internal data | Read-only data <sub>(1)</sub> | Custom data | Restricted by CLP, ACL | Key |
|
||||
|----------------|---------------|-------------------------------|-------------|------------------------|---------------------|
|
||||
| Internal | r/w | r/w | r/w | no | `maintenanceKey` |
|
||||
| Master | -/- | r/- | r/w | no | `masterKey` |
|
||||
| ReadOnlyMaster | -/- | r/- | r/- | no | `readOnlyMasterKey` |
|
||||
| Session | -/- | r/- | r/w | yes | `sessionToken` |
|
||||
|
||||
<sub>(1) `Parse.Object.createdAt`, `Parse.Object.updatedAt`.</sub>
|
||||
|
||||
## Email Verification and Password Reset
|
||||
|
||||
|
||||
@@ -1,3 +1,561 @@
|
||||
# [7.1.0-alpha.16](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.15...7.1.0-alpha.16) (2024-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for dot notation on array fields of Parse Object ([#9115](https://github.com/parse-community/parse-server/issues/9115)) ([cf4c880](https://github.com/parse-community/parse-server/commit/cf4c8807b9da87a0a5f9c94e5bdfcf17cda80cf4))
|
||||
|
||||
# [7.1.0-alpha.15](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.14...7.1.0-alpha.15) (2024-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to @parse/push-adapter 6.4.0 ([#9182](https://github.com/parse-community/parse-server/issues/9182)) ([ef1634b](https://github.com/parse-community/parse-server/commit/ef1634bf1f360429108d29b08032fc7961ff96a1))
|
||||
|
||||
# [7.1.0-alpha.14](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.13...7.1.0-alpha.14) (2024-07-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to Parse JS SDK 5.3.0 ([#9180](https://github.com/parse-community/parse-server/issues/9180)) ([dca187f](https://github.com/parse-community/parse-server/commit/dca187f91b93cbb362b22a3fb9ee38451799ff13))
|
||||
|
||||
# [7.1.0-alpha.13](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.12...7.1.0-alpha.13) (2024-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Invalid push notification tokens are not cleaned up from database for FCM API v2 ([#9173](https://github.com/parse-community/parse-server/issues/9173)) ([284da09](https://github.com/parse-community/parse-server/commit/284da09f4546356b37511a589fb5f64a3efffe79))
|
||||
|
||||
# [7.1.0-alpha.12](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.11...7.1.0-alpha.12) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
|
||||
|
||||
# [7.1.0-alpha.11](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.10...7.1.0-alpha.11) (2024-06-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
|
||||
|
||||
# [7.1.0-alpha.10](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.9...7.1.0-alpha.10) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
|
||||
|
||||
# [7.1.0-alpha.9](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.8...7.1.0-alpha.9) (2024-05-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
|
||||
|
||||
# [7.1.0-alpha.8](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.7...7.1.0-alpha.8) (2024-05-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
|
||||
|
||||
# [7.1.0-alpha.7](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.6...7.1.0-alpha.7) (2024-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
|
||||
|
||||
# [7.1.0-alpha.6](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.5...7.1.0-alpha.6) (2024-04-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
|
||||
|
||||
# [7.1.0-alpha.5](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.4...7.1.0-alpha.5) (2024-04-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
|
||||
|
||||
# [7.1.0-alpha.4](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.3...7.1.0-alpha.4) (2024-03-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
|
||||
|
||||
# [7.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.2...7.1.0-alpha.3) (2024-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
|
||||
|
||||
# [7.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.1...7.1.0-alpha.2) (2024-03-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
|
||||
|
||||
# [7.1.0-alpha.1](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0-alpha.1) (2024-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
|
||||
# [7.0.0-alpha.31](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.30...7.0.0-alpha.31) (2024-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
|
||||
# [7.0.0-alpha.30](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.29...7.0.0-alpha.30) (2024-03-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
|
||||
# [7.0.0-alpha.29](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.28...7.0.0-alpha.29) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9024](https://github.com/parse-community/parse-server/issues/9024)) ([9f6e342](https://github.com/parse-community/parse-server/commit/9f6e3429d3b326cf4e2994733c618d08032fac6e))
|
||||
|
||||
# [7.0.0-alpha.28](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.27...7.0.0-alpha.28) (2024-03-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to Parse JS SDK 5 ([#9022](https://github.com/parse-community/parse-server/issues/9022)) ([ad4aa83](https://github.com/parse-community/parse-server/commit/ad4aa83983205a0e27639f6ee6a4a5963b67e4b8))
|
||||
|
||||
# [7.0.0-alpha.27](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.26...7.0.0-alpha.27) (2024-03-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CacheAdapter does not connect when using a CacheAdapter with a JSON config ([#8633](https://github.com/parse-community/parse-server/issues/8633)) ([720d24e](https://github.com/parse-community/parse-server/commit/720d24e18540da35d50957f17be878316ec30318))
|
||||
|
||||
# [7.0.0-alpha.26](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.25...7.0.0-alpha.26) (2024-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileExtensions` default value rejects file extensions that are less than 3 or more than 4 characters long ([#8699](https://github.com/parse-community/parse-server/issues/8699)) ([2760381](https://github.com/parse-community/parse-server/commit/276038118377c2b22381bcd8d30337203822121b))
|
||||
|
||||
# [7.0.0-alpha.25](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.24...7.0.0-alpha.25) (2024-03-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS5: Config option `allowClientClassCreation` defaults to `false` ([#8849](https://github.com/parse-community/parse-server/issues/8849)) ([29624e0](https://github.com/parse-community/parse-server/commit/29624e0fae17161cd412ae58d35a195cfa286cad))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `allowClientClassCreation` defaults to `false`. ([29624e0](29624e0))
|
||||
|
||||
# [7.0.0-alpha.24](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.23...7.0.0-alpha.24) (2024-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Docker version releases by removing arm/v6 and arm/v7 support ([#8976](https://github.com/parse-community/parse-server/issues/8976)) ([1f62dd0](https://github.com/parse-community/parse-server/commit/1f62dd0f4e107b22a387692558a042ee26ce8703))
|
||||
|
||||
# [7.0.0-alpha.23](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.22...7.0.0-alpha.23) (2024-03-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB query comment ([#8928](https://github.com/parse-community/parse-server/issues/8928)) ([2170962](https://github.com/parse-community/parse-server/commit/2170962a50fa353ed85eda3f11dce7ee3647b087))
|
||||
|
||||
# [7.0.0-alpha.22](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.21...7.0.0-alpha.22) (2024-03-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Switch GraphQL server from Yoga v2 to Apollo v4 ([#8959](https://github.com/parse-community/parse-server/issues/8959)) ([105ae7c](https://github.com/parse-community/parse-server/commit/105ae7c8a57d5a650b243174a80c26bf6db16e28))
|
||||
|
||||
# [7.0.0-alpha.21](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.20...7.0.0-alpha.21) (2024-03-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Deny request if master key is not set in Parse Server option `masterKeyIps` regardless of ACL and CLP ([#8957](https://github.com/parse-community/parse-server/issues/8957)) ([a7b5b38](https://github.com/parse-community/parse-server/commit/a7b5b38418cbed9be3f4a7665f25b97f592663e1))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* A request using the master key will now be rejected as unauthorized if the IP from which the request originates is not set in the Parse Server option `masterKeyIps`, even if the request does not require the master key permission, for example for a public object in a public class class. ([a7b5b38](a7b5b38))
|
||||
|
||||
# [7.0.0-alpha.20](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.19...7.0.0-alpha.20) (2024-03-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8961](https://github.com/parse-community/parse-server/issues/8961)) ([cbefe77](https://github.com/parse-community/parse-server/commit/cbefe770a7260b54748a058b8a7389937dc35833))
|
||||
|
||||
# [7.0.0-alpha.19](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.18...7.0.0-alpha.19) (2024-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Node process exits with error code 1 on uncaught exception to allow custom uncaught exception handling ([#8894](https://github.com/parse-community/parse-server/issues/8894)) ([70c280c](https://github.com/parse-community/parse-server/commit/70c280ca578ff28b5acf92f37fbe06d42a5b34ca))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Node process now exits with code 1 on uncaught exceptions, enabling custom handlers that were blocked by Parse Server's default behavior of re-throwing errors. This change may lead to automatic process restarts by the environment, unlike before. ([70c280c](70c280c))
|
||||
|
||||
# [7.0.0-alpha.18](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.17...7.0.0-alpha.18) (2024-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS6: Authentication adapters disabled by default ([#8858](https://github.com/parse-community/parse-server/issues/8858)) ([0cf58eb](https://github.com/parse-community/parse-server/commit/0cf58eb8d60c8e5f485764e154f3214c49eee430))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Authentication adapters are disabled by default; to use an authentication adapter it needs to be explicitly enabled in the Parse Server authentication adapter option `auth.<provider>.enabled: true` ([0cf58eb](0cf58eb))
|
||||
|
||||
# [7.0.0-alpha.17](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.16...7.0.0-alpha.17) (2024-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS8: Parse Server option `allowExpiredAuthDataToken` defaults to `false` ([#8860](https://github.com/parse-community/parse-server/issues/8860)) ([e29845f](https://github.com/parse-community/parse-server/commit/e29845f8dacac09ce3093d75c0d92330c24389e8))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Parse Server option `allowExpiredAuthDataToken` defaults to `false`; a 3rd party authentication token will be validated every time the user tries to log in and the login will fail if the token has expired; the effect of this change may differ for different authentication adapters, depending on the token lifetime and the token refresh logic of the adapter ([e29845f](e29845f))
|
||||
|
||||
# [7.0.0-alpha.16](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.15...7.0.0-alpha.16) (2024-02-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS9: LiveQuery `fields` option is renamed to `keys` ([#8852](https://github.com/parse-community/parse-server/issues/8852)) ([38983e8](https://github.com/parse-community/parse-server/commit/38983e8e9b5cdbd006f311a2338103624137d013))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* LiveQuery `fields` option is renamed to `keys` ([38983e8](38983e8))
|
||||
|
||||
# [7.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.14...7.0.0-alpha.15) (2024-02-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS7: Remove deprecated Cloud Code file trigger syntax ([#8855](https://github.com/parse-community/parse-server/issues/8855)) ([4e6a375](https://github.com/parse-community/parse-server/commit/4e6a375b5184ae0f7aa256a921eca4021c609435))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Cloud Code file trigger syntax has been aligned with object trigger syntax, for example `Parse.Cloud.beforeDeleteFile'` has been changed to `Parse.Cloud.beforeDelete(Parse.File, (request) => {})'` ([4e6a375](4e6a375))
|
||||
|
||||
# [7.0.0-alpha.14](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.13...7.0.0-alpha.14) (2024-02-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* GraphQL file upload fails in case of use of pointer or relation ([#8721](https://github.com/parse-community/parse-server/issues/8721)) ([1aba638](https://github.com/parse-community/parse-server/commit/1aba6382c873fb489d4a898d301e6da9fb6aa61b))
|
||||
|
||||
# [7.0.0-alpha.13](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.12...7.0.0-alpha.13) (2024-02-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Docker image not published to Docker Hub on new release ([#8905](https://github.com/parse-community/parse-server/issues/8905)) ([a2ac8d1](https://github.com/parse-community/parse-server/commit/a2ac8d133c71cd7b61e5ef59c4be915cfea85db6))
|
||||
|
||||
# [7.0.0-alpha.12](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.11...7.0.0-alpha.12) (2024-02-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for Node 20, drop support for Node 14, 16 ([#8907](https://github.com/parse-community/parse-server/issues/8907)) ([ced4872](https://github.com/parse-community/parse-server/commit/ced487246ea0ef72a8aa014991f003209b34841e))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Removes support for Node 14 and 16 ([ced4872](ced4872))
|
||||
|
||||
# [7.0.0-alpha.11](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.10...7.0.0-alpha.11) (2024-01-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for Postgres 16 ([#8898](https://github.com/parse-community/parse-server/issues/8898)) ([99489b2](https://github.com/parse-community/parse-server/commit/99489b22e4f0982e6cb39992974b51aa8d3a31e4))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Removes support for Postgres 11 and 12 ([99489b2](99489b2))
|
||||
|
||||
# [7.0.0-alpha.10](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.9...7.0.0-alpha.10) (2024-01-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add password validation via POST request for user with unverified email using master key and option `ignoreEmailVerification` ([#8895](https://github.com/parse-community/parse-server/issues/8895)) ([633a9d2](https://github.com/parse-community/parse-server/commit/633a9d25e4253e2125bc93c02ee8a37e0f5f7b83))
|
||||
|
||||
# [7.0.0-alpha.9](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.8...7.0.0-alpha.9) (2024-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#8784](https://github.com/parse-community/parse-server/issues/8784)) ([66e3603](https://github.com/parse-community/parse-server/commit/66e36039d8af654cfa0284666c0ddd94975dcb52))
|
||||
|
||||
# [7.0.0-alpha.8](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.7...7.0.0-alpha.8) (2024-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Incomplete user object in `verifyEmail` function if both username and email are changed ([#8889](https://github.com/parse-community/parse-server/issues/8889)) ([1eb95ae](https://github.com/parse-community/parse-server/commit/1eb95aeb41a96250e582d79a703f6adcb403c08b))
|
||||
|
||||
# [7.0.0-alpha.7](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.6...7.0.0-alpha.7) (2024-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Username is `undefined` in email verification link on email change ([#8887](https://github.com/parse-community/parse-server/issues/8887)) ([e315c13](https://github.com/parse-community/parse-server/commit/e315c137bf41bedfa8f0df537f2c3f6ab45b7e60))
|
||||
|
||||
# [7.0.0-alpha.6](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.5...7.0.0-alpha.6) (2024-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `emailVerifyTokenReuseIfValid: true` generates new token on every email verification request ([#8885](https://github.com/parse-community/parse-server/issues/8885)) ([0023ce4](https://github.com/parse-community/parse-server/commit/0023ce448a5e9423337d0e1a25648bde1156bc95))
|
||||
|
||||
# [7.0.0-alpha.5](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.4...7.0.0-alpha.5) (2024-01-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `installationId`, `ip`, `resendRequest` to arguments passed to `verifyUserEmails` on verification email request ([#8873](https://github.com/parse-community/parse-server/issues/8873)) ([8adcbee](https://github.com/parse-community/parse-server/commit/8adcbee11283d3e95179ca2047e2615f52c18806))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The `Parse.User` passed as argument if `verifyUserEmails` is set to a function is renamed from `user` to `object` for consistency with invocations of `verifyUserEmails` on signup or login; the user object is not a plain JavaScript object anymore but an instance of `Parse.User` ([8adcbee](8adcbee))
|
||||
|
||||
# [7.0.0-alpha.4](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.3...7.0.0-alpha.4) (2023-12-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `Parse.User` as function parameter to Parse Server options `verifyUserEmails`, `preventLoginWithUnverifiedEmail` on login ([#8850](https://github.com/parse-community/parse-server/issues/8850)) ([972f630](https://github.com/parse-community/parse-server/commit/972f6300163b3cd7d95eeb95986e8322c95f821c))
|
||||
|
||||
# [7.0.0-alpha.3](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.2...7.0.0-alpha.3) (2023-12-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions ([#8838](https://github.com/parse-community/parse-server/issues/8838)) ([8e7a6b1](https://github.com/parse-community/parse-server/commit/8e7a6b1480c0117e6c73e7adc5a6619115a04e85))
|
||||
|
||||
### Features
|
||||
|
||||
* Allow `Parse.Session.current` on expired session token instead of throwing error ([#8722](https://github.com/parse-community/parse-server/issues/8722)) ([f9dde4a](https://github.com/parse-community/parse-server/commit/f9dde4a9f8a90c63f71172c9bc515b0f6c6d2e4a))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* `Parse.Session.current()` no longer throws an error if the session token is expired, but instead returns the session token with its expiration date to allow checking its validity ([f9dde4a](f9dde4a))
|
||||
|
||||
# [7.0.0-alpha.2](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.1...7.0.0-alpha.2) (2023-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `installationId` to arguments for `verifyUserEmails`, `preventLoginWithUnverifiedEmail` ([#8836](https://github.com/parse-community/parse-server/issues/8836)) ([a22dbe1](https://github.com/parse-community/parse-server/commit/a22dbe16d5ac0090608f6caaf0ebd134925b7fd4))
|
||||
|
||||
# [7.0.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.5.0-alpha.2...7.0.0-alpha.1) (2023-12-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB 7 ([#8761](https://github.com/parse-community/parse-server/issues/8761)) ([3de8494](https://github.com/parse-community/parse-server/commit/3de8494a221991dfd10a74e0a2dc89576265c9b7))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* `Parse.Query` no longer supports the BSON type `code`; although this feature was never officially documented, its removal is announced as a breaking change to protect deployments where it might be in use. ([3de8494](3de8494))
|
||||
|
||||
# [6.5.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.5.0-alpha.1...6.5.0-alpha.2) (2023-11-19)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improved IP validation performance for `masterKeyIPs`, `maintenanceKeyIPs` ([#8510](https://github.com/parse-community/parse-server/issues/8510)) ([b87daba](https://github.com/parse-community/parse-server/commit/b87daba0671a1b0b7b8d63bc671d665c91a04522))
|
||||
|
||||
# [6.5.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.4.0...6.5.0-alpha.1) (2023-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
* Upgrade Parse Server Push Adapter to 5.0.2 ([#8813](https://github.com/parse-community/parse-server/issues/8813)) ([6ef1986](https://github.com/parse-community/parse-server/commit/6ef1986c03a1d84b7e11c05851e5bf9688d88740))
|
||||
|
||||
# [6.4.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.7...6.4.0-alpha.8) (2023-11-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
|
||||
# [6.4.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.6...6.4.0-alpha.7) (2023-10-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
|
||||
# [6.4.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.5...6.4.0-alpha.6) (2023-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
|
||||
# [6.4.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.4...6.4.0-alpha.5) (2023-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
|
||||
# [6.4.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.3...6.4.0-alpha.4) (2023-09-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
|
||||
# [6.4.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.2...6.4.0-alpha.3) (2023-09-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
|
||||
# [6.4.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.1...6.4.0-alpha.2) (2023-09-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
|
||||
# [6.4.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.4.0-alpha.1) (2023-09-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
|
||||
# [6.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.8...6.3.0-alpha.9) (2023-09-13)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd))
|
||||
|
||||
# [6.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.7...6.3.0-alpha.8) (2023-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49))
|
||||
|
||||
# [6.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.6...6.3.0-alpha.7) (2023-08-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec))
|
||||
|
||||
# [6.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.5...6.3.0-alpha.6) (2023-07-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b))
|
||||
|
||||
# [6.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.4...6.3.0-alpha.5) (2023-07-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298))
|
||||
|
||||
# [6.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.3...6.3.0-alpha.4) (2023-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64))
|
||||
|
||||
# [6.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.2...6.3.0-alpha.3) (2023-06-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589))
|
||||
|
||||
# [6.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.1...6.3.0-alpha.2) (2023-06-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions ([#8425](https://github.com/parse-community/parse-server/issues/8425)) ([44acd6d](https://github.com/parse-community/parse-server/commit/44acd6d9ed157ad4842200c9d01f9c77a05fec3a))
|
||||
|
||||
# [6.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.3.0-alpha.1) (2023-06-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
|
||||
* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
|
||||
* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
|
||||
* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
|
||||
* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
|
||||
* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
|
||||
|
||||
### Features
|
||||
|
||||
* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
|
||||
* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
|
||||
* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
|
||||
* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
|
||||
* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
|
||||
* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
|
||||
|
||||
# [6.1.0-alpha.20](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.19...6.1.0-alpha.20) (2023-06-09)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,135 @@
|
||||
# [7.2.0-beta.1](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0-beta.1) (2024-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Invalid push notification tokens are not cleaned up from database for FCM API v2 ([#9173](https://github.com/parse-community/parse-server/issues/9173)) ([284da09](https://github.com/parse-community/parse-server/commit/284da09f4546356b37511a589fb5f64a3efffe79))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for dot notation on array fields of Parse Object ([#9115](https://github.com/parse-community/parse-server/issues/9115)) ([cf4c880](https://github.com/parse-community/parse-server/commit/cf4c8807b9da87a0a5f9c94e5bdfcf17cda80cf4))
|
||||
* Upgrade to @parse/push-adapter 6.4.0 ([#9182](https://github.com/parse-community/parse-server/issues/9182)) ([ef1634b](https://github.com/parse-community/parse-server/commit/ef1634bf1f360429108d29b08032fc7961ff96a1))
|
||||
* Upgrade to Parse JS SDK 5.3.0 ([#9180](https://github.com/parse-community/parse-server/issues/9180)) ([dca187f](https://github.com/parse-community/parse-server/commit/dca187f91b93cbb362b22a3fb9ee38451799ff13))
|
||||
|
||||
# [7.1.0-beta.1](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0-beta.1) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
|
||||
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
|
||||
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
|
||||
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
|
||||
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
|
||||
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
|
||||
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
|
||||
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
|
||||
|
||||
# [7.0.0-beta.1](https://github.com/parse-community/parse-server/compare/6.5.0-beta.1...7.0.0-beta.1) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CacheAdapter does not connect when using a CacheAdapter with a JSON config ([#8633](https://github.com/parse-community/parse-server/issues/8633)) ([720d24e](https://github.com/parse-community/parse-server/commit/720d24e18540da35d50957f17be878316ec30318))
|
||||
* Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions ([#8838](https://github.com/parse-community/parse-server/issues/8838)) ([8e7a6b1](https://github.com/parse-community/parse-server/commit/8e7a6b1480c0117e6c73e7adc5a6619115a04e85))
|
||||
* Deny request if master key is not set in Parse Server option `masterKeyIps` regardless of ACL and CLP ([#8957](https://github.com/parse-community/parse-server/issues/8957)) ([a7b5b38](https://github.com/parse-community/parse-server/commit/a7b5b38418cbed9be3f4a7665f25b97f592663e1))
|
||||
* Docker image not published to Docker Hub on new release ([#8905](https://github.com/parse-community/parse-server/issues/8905)) ([a2ac8d1](https://github.com/parse-community/parse-server/commit/a2ac8d133c71cd7b61e5ef59c4be915cfea85db6))
|
||||
* Docker version releases by removing arm/v6 and arm/v7 support ([#8976](https://github.com/parse-community/parse-server/issues/8976)) ([1f62dd0](https://github.com/parse-community/parse-server/commit/1f62dd0f4e107b22a387692558a042ee26ce8703))
|
||||
* GraphQL file upload fails in case of use of pointer or relation ([#8721](https://github.com/parse-community/parse-server/issues/8721)) ([1aba638](https://github.com/parse-community/parse-server/commit/1aba6382c873fb489d4a898d301e6da9fb6aa61b))
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8961](https://github.com/parse-community/parse-server/issues/8961)) ([cbefe77](https://github.com/parse-community/parse-server/commit/cbefe770a7260b54748a058b8a7389937dc35833))
|
||||
* Incomplete user object in `verifyEmail` function if both username and email are changed ([#8889](https://github.com/parse-community/parse-server/issues/8889)) ([1eb95ae](https://github.com/parse-community/parse-server/commit/1eb95aeb41a96250e582d79a703f6adcb403c08b))
|
||||
* Parse Server option `emailVerifyTokenReuseIfValid: true` generates new token on every email verification request ([#8885](https://github.com/parse-community/parse-server/issues/8885)) ([0023ce4](https://github.com/parse-community/parse-server/commit/0023ce448a5e9423337d0e1a25648bde1156bc95))
|
||||
* Parse Server option `fileExtensions` default value rejects file extensions that are less than 3 or more than 4 characters long ([#8699](https://github.com/parse-community/parse-server/issues/8699)) ([2760381](https://github.com/parse-community/parse-server/commit/276038118377c2b22381bcd8d30337203822121b))
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9024](https://github.com/parse-community/parse-server/issues/9024)) ([9f6e342](https://github.com/parse-community/parse-server/commit/9f6e3429d3b326cf4e2994733c618d08032fac6e))
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#8784](https://github.com/parse-community/parse-server/issues/8784)) ([66e3603](https://github.com/parse-community/parse-server/commit/66e36039d8af654cfa0284666c0ddd94975dcb52))
|
||||
* Username is `undefined` in email verification link on email change ([#8887](https://github.com/parse-community/parse-server/issues/8887)) ([e315c13](https://github.com/parse-community/parse-server/commit/e315c137bf41bedfa8f0df537f2c3f6ab45b7e60))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `installationId` to arguments for `verifyUserEmails`, `preventLoginWithUnverifiedEmail` ([#8836](https://github.com/parse-community/parse-server/issues/8836)) ([a22dbe1](https://github.com/parse-community/parse-server/commit/a22dbe16d5ac0090608f6caaf0ebd134925b7fd4))
|
||||
* Add `installationId`, `ip`, `resendRequest` to arguments passed to `verifyUserEmails` on verification email request ([#8873](https://github.com/parse-community/parse-server/issues/8873)) ([8adcbee](https://github.com/parse-community/parse-server/commit/8adcbee11283d3e95179ca2047e2615f52c18806))
|
||||
* Add `Parse.User` as function parameter to Parse Server options `verifyUserEmails`, `preventLoginWithUnverifiedEmail` on login ([#8850](https://github.com/parse-community/parse-server/issues/8850)) ([972f630](https://github.com/parse-community/parse-server/commit/972f6300163b3cd7d95eeb95986e8322c95f821c))
|
||||
* Add password validation via POST request for user with unverified email using master key and option `ignoreEmailVerification` ([#8895](https://github.com/parse-community/parse-server/issues/8895)) ([633a9d2](https://github.com/parse-community/parse-server/commit/633a9d25e4253e2125bc93c02ee8a37e0f5f7b83))
|
||||
* Add support for MongoDB 7 ([#8761](https://github.com/parse-community/parse-server/issues/8761)) ([3de8494](https://github.com/parse-community/parse-server/commit/3de8494a221991dfd10a74e0a2dc89576265c9b7))
|
||||
* Add support for MongoDB query comment ([#8928](https://github.com/parse-community/parse-server/issues/8928)) ([2170962](https://github.com/parse-community/parse-server/commit/2170962a50fa353ed85eda3f11dce7ee3647b087))
|
||||
* Add support for Node 20, drop support for Node 14, 16 ([#8907](https://github.com/parse-community/parse-server/issues/8907)) ([ced4872](https://github.com/parse-community/parse-server/commit/ced487246ea0ef72a8aa014991f003209b34841e))
|
||||
* Add support for Postgres 16 ([#8898](https://github.com/parse-community/parse-server/issues/8898)) ([99489b2](https://github.com/parse-community/parse-server/commit/99489b22e4f0982e6cb39992974b51aa8d3a31e4))
|
||||
* Allow `Parse.Session.current` on expired session token instead of throwing error ([#8722](https://github.com/parse-community/parse-server/issues/8722)) ([f9dde4a](https://github.com/parse-community/parse-server/commit/f9dde4a9f8a90c63f71172c9bc515b0f6c6d2e4a))
|
||||
* Deprecation DEPPS5: Config option `allowClientClassCreation` defaults to `false` ([#8849](https://github.com/parse-community/parse-server/issues/8849)) ([29624e0](https://github.com/parse-community/parse-server/commit/29624e0fae17161cd412ae58d35a195cfa286cad))
|
||||
* Deprecation DEPPS6: Authentication adapters disabled by default ([#8858](https://github.com/parse-community/parse-server/issues/8858)) ([0cf58eb](https://github.com/parse-community/parse-server/commit/0cf58eb8d60c8e5f485764e154f3214c49eee430))
|
||||
* Deprecation DEPPS7: Remove deprecated Cloud Code file trigger syntax ([#8855](https://github.com/parse-community/parse-server/issues/8855)) ([4e6a375](https://github.com/parse-community/parse-server/commit/4e6a375b5184ae0f7aa256a921eca4021c609435))
|
||||
* Deprecation DEPPS8: Parse Server option `allowExpiredAuthDataToken` defaults to `false` ([#8860](https://github.com/parse-community/parse-server/issues/8860)) ([e29845f](https://github.com/parse-community/parse-server/commit/e29845f8dacac09ce3093d75c0d92330c24389e8))
|
||||
* Deprecation DEPPS9: LiveQuery `fields` option is renamed to `keys` ([#8852](https://github.com/parse-community/parse-server/issues/8852)) ([38983e8](https://github.com/parse-community/parse-server/commit/38983e8e9b5cdbd006f311a2338103624137d013))
|
||||
* Node process exits with error code 1 on uncaught exception to allow custom uncaught exception handling ([#8894](https://github.com/parse-community/parse-server/issues/8894)) ([70c280c](https://github.com/parse-community/parse-server/commit/70c280ca578ff28b5acf92f37fbe06d42a5b34ca))
|
||||
* Switch GraphQL server from Yoga v2 to Apollo v4 ([#8959](https://github.com/parse-community/parse-server/issues/8959)) ([105ae7c](https://github.com/parse-community/parse-server/commit/105ae7c8a57d5a650b243174a80c26bf6db16e28))
|
||||
* Upgrade Parse Server Push Adapter to 5.0.2 ([#8813](https://github.com/parse-community/parse-server/issues/8813)) ([6ef1986](https://github.com/parse-community/parse-server/commit/6ef1986c03a1d84b7e11c05851e5bf9688d88740))
|
||||
* Upgrade to Parse JS SDK 5 ([#9022](https://github.com/parse-community/parse-server/issues/9022)) ([ad4aa83](https://github.com/parse-community/parse-server/commit/ad4aa83983205a0e27639f6ee6a4a5963b67e4b8))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improved IP validation performance for `masterKeyIPs`, `maintenanceKeyIPs` ([#8510](https://github.com/parse-community/parse-server/issues/8510)) ([b87daba](https://github.com/parse-community/parse-server/commit/b87daba0671a1b0b7b8d63bc671d665c91a04522))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `allowClientClassCreation` defaults to `false`. ([29624e0](29624e0))
|
||||
* A request using the master key will now be rejected as unauthorized if the IP from which the request originates is not set in the Parse Server option `masterKeyIps`, even if the request does not require the master key permission, for example for a public object in a public class class. ([a7b5b38](a7b5b38))
|
||||
* Node process now exits with code 1 on uncaught exceptions, enabling custom handlers that were blocked by Parse Server's default behavior of re-throwing errors. This change may lead to automatic process restarts by the environment, unlike before. ([70c280c](70c280c))
|
||||
* Authentication adapters are disabled by default; to use an authentication adapter it needs to be explicitly enabled in the Parse Server authentication adapter option `auth.<provider>.enabled: true` ([0cf58eb](0cf58eb))
|
||||
* Parse Server option `allowExpiredAuthDataToken` defaults to `false`; a 3rd party authentication token will be validated every time the user tries to log in and the login will fail if the token has expired; the effect of this change may differ for different authentication adapters, depending on the token lifetime and the token refresh logic of the adapter ([e29845f](e29845f))
|
||||
* LiveQuery `fields` option is renamed to `keys` ([38983e8](38983e8))
|
||||
* Cloud Code file trigger syntax has been aligned with object trigger syntax, for example `Parse.Cloud.beforeDeleteFile'` has been changed to `Parse.Cloud.beforeDelete(Parse.File, (request) => {})'` ([4e6a375](4e6a375))
|
||||
* Removes support for Node 14 and 16 ([ced4872](ced4872))
|
||||
* Removes support for Postgres 11 and 12 ([99489b2](99489b2))
|
||||
* The `Parse.User` passed as argument if `verifyUserEmails` is set to a function is renamed from `user` to `object` for consistency with invocations of `verifyUserEmails` on signup or login; the user object is not a plain JavaScript object anymore but an instance of `Parse.User` ([8adcbee](8adcbee))
|
||||
* `Parse.Session.current()` no longer throws an error if the session token is expired, but instead returns the session token with its expiration date to allow checking its validity ([f9dde4a](f9dde4a))
|
||||
* `Parse.Query` no longer supports the BSON type `code`; although this feature was never officially documented, its removal is announced as a breaking change to protect deployments where it might be in use. ([3de8494](3de8494))
|
||||
|
||||
# [6.5.0-beta.1](https://github.com/parse-community/parse-server/compare/6.4.0...6.5.0-beta.1) (2023-11-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
|
||||
# [6.4.0-beta.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.4.0-beta.1) (2023-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b))
|
||||
* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49))
|
||||
* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec))
|
||||
* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64))
|
||||
|
||||
### Features
|
||||
|
||||
* Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions ([#8425](https://github.com/parse-community/parse-server/issues/8425)) ([44acd6d](https://github.com/parse-community/parse-server/commit/44acd6d9ed157ad4842200c9d01f9c77a05fec3a))
|
||||
* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298))
|
||||
* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd))
|
||||
|
||||
# [6.3.0-beta.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.3.0-beta.1) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,172 @@
|
||||
# [7.2.0](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0) (2024-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Invalid push notification tokens are not cleaned up from database for FCM API v2 ([#9173](https://github.com/parse-community/parse-server/issues/9173)) ([284da09](https://github.com/parse-community/parse-server/commit/284da09f4546356b37511a589fb5f64a3efffe79))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for dot notation on array fields of Parse Object ([#9115](https://github.com/parse-community/parse-server/issues/9115)) ([cf4c880](https://github.com/parse-community/parse-server/commit/cf4c8807b9da87a0a5f9c94e5bdfcf17cda80cf4))
|
||||
* Upgrade to @parse/push-adapter 6.4.0 ([#9182](https://github.com/parse-community/parse-server/issues/9182)) ([ef1634b](https://github.com/parse-community/parse-server/commit/ef1634bf1f360429108d29b08032fc7961ff96a1))
|
||||
* Upgrade to Parse JS SDK 5.3.0 ([#9180](https://github.com/parse-community/parse-server/issues/9180)) ([dca187f](https://github.com/parse-community/parse-server/commit/dca187f91b93cbb362b22a3fb9ee38451799ff13))
|
||||
|
||||
# [7.1.0](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
|
||||
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
|
||||
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
|
||||
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
|
||||
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
|
||||
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
|
||||
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
|
||||
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
|
||||
|
||||
# [7.0.0](https://github.com/parse-community/parse-server/compare/6.4.0...7.0.0) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CacheAdapter does not connect when using a CacheAdapter with a JSON config ([#8633](https://github.com/parse-community/parse-server/issues/8633)) ([720d24e](https://github.com/parse-community/parse-server/commit/720d24e18540da35d50957f17be878316ec30318))
|
||||
* Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions ([#8838](https://github.com/parse-community/parse-server/issues/8838)) ([8e7a6b1](https://github.com/parse-community/parse-server/commit/8e7a6b1480c0117e6c73e7adc5a6619115a04e85))
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
* Deny request if master key is not set in Parse Server option `masterKeyIps` regardless of ACL and CLP ([#8957](https://github.com/parse-community/parse-server/issues/8957)) ([a7b5b38](https://github.com/parse-community/parse-server/commit/a7b5b38418cbed9be3f4a7665f25b97f592663e1))
|
||||
* Docker image not published to Docker Hub on new release ([#8905](https://github.com/parse-community/parse-server/issues/8905)) ([a2ac8d1](https://github.com/parse-community/parse-server/commit/a2ac8d133c71cd7b61e5ef59c4be915cfea85db6))
|
||||
* Docker version releases by removing arm/v6 and arm/v7 support ([#8976](https://github.com/parse-community/parse-server/issues/8976)) ([1f62dd0](https://github.com/parse-community/parse-server/commit/1f62dd0f4e107b22a387692558a042ee26ce8703))
|
||||
* GraphQL file upload fails in case of use of pointer or relation ([#8721](https://github.com/parse-community/parse-server/issues/8721)) ([1aba638](https://github.com/parse-community/parse-server/commit/1aba6382c873fb489d4a898d301e6da9fb6aa61b))
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8961](https://github.com/parse-community/parse-server/issues/8961)) ([cbefe77](https://github.com/parse-community/parse-server/commit/cbefe770a7260b54748a058b8a7389937dc35833))
|
||||
* Incomplete user object in `verifyEmail` function if both username and email are changed ([#8889](https://github.com/parse-community/parse-server/issues/8889)) ([1eb95ae](https://github.com/parse-community/parse-server/commit/1eb95aeb41a96250e582d79a703f6adcb403c08b))
|
||||
* Parse Server option `emailVerifyTokenReuseIfValid: true` generates new token on every email verification request ([#8885](https://github.com/parse-community/parse-server/issues/8885)) ([0023ce4](https://github.com/parse-community/parse-server/commit/0023ce448a5e9423337d0e1a25648bde1156bc95))
|
||||
* Parse Server option `fileExtensions` default value rejects file extensions that are less than 3 or more than 4 characters long ([#8699](https://github.com/parse-community/parse-server/issues/8699)) ([2760381](https://github.com/parse-community/parse-server/commit/276038118377c2b22381bcd8d30337203822121b))
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9024](https://github.com/parse-community/parse-server/issues/9024)) ([9f6e342](https://github.com/parse-community/parse-server/commit/9f6e3429d3b326cf4e2994733c618d08032fac6e))
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#8784](https://github.com/parse-community/parse-server/issues/8784)) ([66e3603](https://github.com/parse-community/parse-server/commit/66e36039d8af654cfa0284666c0ddd94975dcb52))
|
||||
* Username is `undefined` in email verification link on email change ([#8887](https://github.com/parse-community/parse-server/issues/8887)) ([e315c13](https://github.com/parse-community/parse-server/commit/e315c137bf41bedfa8f0df537f2c3f6ab45b7e60))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
* Add `installationId` to arguments for `verifyUserEmails`, `preventLoginWithUnverifiedEmail` ([#8836](https://github.com/parse-community/parse-server/issues/8836)) ([a22dbe1](https://github.com/parse-community/parse-server/commit/a22dbe16d5ac0090608f6caaf0ebd134925b7fd4))
|
||||
* Add `installationId`, `ip`, `resendRequest` to arguments passed to `verifyUserEmails` on verification email request ([#8873](https://github.com/parse-community/parse-server/issues/8873)) ([8adcbee](https://github.com/parse-community/parse-server/commit/8adcbee11283d3e95179ca2047e2615f52c18806))
|
||||
* Add `Parse.User` as function parameter to Parse Server options `verifyUserEmails`, `preventLoginWithUnverifiedEmail` on login ([#8850](https://github.com/parse-community/parse-server/issues/8850)) ([972f630](https://github.com/parse-community/parse-server/commit/972f6300163b3cd7d95eeb95986e8322c95f821c))
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
* Add password validation via POST request for user with unverified email using master key and option `ignoreEmailVerification` ([#8895](https://github.com/parse-community/parse-server/issues/8895)) ([633a9d2](https://github.com/parse-community/parse-server/commit/633a9d25e4253e2125bc93c02ee8a37e0f5f7b83))
|
||||
* Add support for MongoDB 7 ([#8761](https://github.com/parse-community/parse-server/issues/8761)) ([3de8494](https://github.com/parse-community/parse-server/commit/3de8494a221991dfd10a74e0a2dc89576265c9b7))
|
||||
* Add support for MongoDB query comment ([#8928](https://github.com/parse-community/parse-server/issues/8928)) ([2170962](https://github.com/parse-community/parse-server/commit/2170962a50fa353ed85eda3f11dce7ee3647b087))
|
||||
* Add support for Node 20, drop support for Node 14, 16 ([#8907](https://github.com/parse-community/parse-server/issues/8907)) ([ced4872](https://github.com/parse-community/parse-server/commit/ced487246ea0ef72a8aa014991f003209b34841e))
|
||||
* Add support for Postgres 16 ([#8898](https://github.com/parse-community/parse-server/issues/8898)) ([99489b2](https://github.com/parse-community/parse-server/commit/99489b22e4f0982e6cb39992974b51aa8d3a31e4))
|
||||
* Allow `Parse.Session.current` on expired session token instead of throwing error ([#8722](https://github.com/parse-community/parse-server/issues/8722)) ([f9dde4a](https://github.com/parse-community/parse-server/commit/f9dde4a9f8a90c63f71172c9bc515b0f6c6d2e4a))
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
* Deprecation DEPPS5: Config option `allowClientClassCreation` defaults to `false` ([#8849](https://github.com/parse-community/parse-server/issues/8849)) ([29624e0](https://github.com/parse-community/parse-server/commit/29624e0fae17161cd412ae58d35a195cfa286cad))
|
||||
* Deprecation DEPPS6: Authentication adapters disabled by default ([#8858](https://github.com/parse-community/parse-server/issues/8858)) ([0cf58eb](https://github.com/parse-community/parse-server/commit/0cf58eb8d60c8e5f485764e154f3214c49eee430))
|
||||
* Deprecation DEPPS7: Remove deprecated Cloud Code file trigger syntax ([#8855](https://github.com/parse-community/parse-server/issues/8855)) ([4e6a375](https://github.com/parse-community/parse-server/commit/4e6a375b5184ae0f7aa256a921eca4021c609435))
|
||||
* Deprecation DEPPS8: Parse Server option `allowExpiredAuthDataToken` defaults to `false` ([#8860](https://github.com/parse-community/parse-server/issues/8860)) ([e29845f](https://github.com/parse-community/parse-server/commit/e29845f8dacac09ce3093d75c0d92330c24389e8))
|
||||
* Deprecation DEPPS9: LiveQuery `fields` option is renamed to `keys` ([#8852](https://github.com/parse-community/parse-server/issues/8852)) ([38983e8](https://github.com/parse-community/parse-server/commit/38983e8e9b5cdbd006f311a2338103624137d013))
|
||||
* Node process exits with error code 1 on uncaught exception to allow custom uncaught exception handling ([#8894](https://github.com/parse-community/parse-server/issues/8894)) ([70c280c](https://github.com/parse-community/parse-server/commit/70c280ca578ff28b5acf92f37fbe06d42a5b34ca))
|
||||
* Switch GraphQL server from Yoga v2 to Apollo v4 ([#8959](https://github.com/parse-community/parse-server/issues/8959)) ([105ae7c](https://github.com/parse-community/parse-server/commit/105ae7c8a57d5a650b243174a80c26bf6db16e28))
|
||||
* Upgrade Parse Server Push Adapter to 5.0.2 ([#8813](https://github.com/parse-community/parse-server/issues/8813)) ([6ef1986](https://github.com/parse-community/parse-server/commit/6ef1986c03a1d84b7e11c05851e5bf9688d88740))
|
||||
* Upgrade to Parse JS SDK 5 ([#9022](https://github.com/parse-community/parse-server/issues/9022)) ([ad4aa83](https://github.com/parse-community/parse-server/commit/ad4aa83983205a0e27639f6ee6a4a5963b67e4b8))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improved IP validation performance for `masterKeyIPs`, `maintenanceKeyIPs` ([#8510](https://github.com/parse-community/parse-server/issues/8510)) ([b87daba](https://github.com/parse-community/parse-server/commit/b87daba0671a1b0b7b8d63bc671d665c91a04522))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `allowClientClassCreation` defaults to `false`. ([29624e0](29624e0))
|
||||
* A request using the master key will now be rejected as unauthorized if the IP from which the request originates is not set in the Parse Server option `masterKeyIps`, even if the request does not require the master key permission, for example for a public object in a public class class. ([a7b5b38](a7b5b38))
|
||||
* Node process now exits with code 1 on uncaught exceptions, enabling custom handlers that were blocked by Parse Server's default behavior of re-throwing errors. This change may lead to automatic process restarts by the environment, unlike before. ([70c280c](70c280c))
|
||||
* Authentication adapters are disabled by default; to use an authentication adapter it needs to be explicitly enabled in the Parse Server authentication adapter option `auth.<provider>.enabled: true` ([0cf58eb](0cf58eb))
|
||||
* Parse Server option `allowExpiredAuthDataToken` defaults to `false`; a 3rd party authentication token will be validated every time the user tries to log in and the login will fail if the token has expired; the effect of this change may differ for different authentication adapters, depending on the token lifetime and the token refresh logic of the adapter ([e29845f](e29845f))
|
||||
* LiveQuery `fields` option is renamed to `keys` ([38983e8](38983e8))
|
||||
* Cloud Code file trigger syntax has been aligned with object trigger syntax, for example `Parse.Cloud.beforeDeleteFile'` has been changed to `Parse.Cloud.beforeDelete(Parse.File, (request) => {})'` ([4e6a375](4e6a375))
|
||||
* Removes support for Node 14 and 16 ([ced4872](ced4872))
|
||||
* Removes support for Postgres 11 and 12 ([99489b2](99489b2))
|
||||
* The `Parse.User` passed as argument if `verifyUserEmails` is set to a function is renamed from `user` to `object` for consistency with invocations of `verifyUserEmails` on signup or login; the user object is not a plain JavaScript object anymore but an instance of `Parse.User` ([8adcbee](8adcbee))
|
||||
* `Parse.Session.current()` no longer throws an error if the session token is expired, but instead returns the session token with its expiration date to allow checking its validity ([f9dde4a](f9dde4a))
|
||||
* `Parse.Query` no longer supports the BSON type `code`; although this feature was never officially documented, its removal is announced as a breaking change to protect deployments where it might be in use. ([3de8494](3de8494))
|
||||
|
||||
# [6.4.0](https://github.com/parse-community/parse-server/compare/6.3.1...6.4.0) (2023-11-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b))
|
||||
* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49))
|
||||
* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec))
|
||||
* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64))
|
||||
|
||||
### Features
|
||||
|
||||
* Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions ([#8425](https://github.com/parse-community/parse-server/issues/8425)) ([44acd6d](https://github.com/parse-community/parse-server/commit/44acd6d9ed157ad4842200c9d01f9c77a05fec3a))
|
||||
* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298))
|
||||
* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd))
|
||||
|
||||
## [6.3.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.3.1) (2023-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server crash when uploading file without extension; fixes security vulnerability [GHSA-792q-q67h-w579](https://github.com/parse-community/parse-server/security/advisories/GHSA-792q-q67h-w579) ([#8781](https://github.com/parse-community/parse-server/issues/8781)) ([fd86278](https://github.com/parse-community/parse-server/commit/fd86278919556d3682e7e2c856dfccd5beffbfc0))
|
||||
|
||||
# [6.3.0](https://github.com/parse-community/parse-server/compare/6.2.2...6.3.0) (2023-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
|
||||
* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
|
||||
* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
|
||||
* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
|
||||
* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
|
||||
* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
|
||||
|
||||
### Features
|
||||
|
||||
* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
|
||||
* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
|
||||
* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
|
||||
* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
|
||||
* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
|
||||
* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
|
||||
|
||||
## [6.2.2](https://github.com/parse-community/parse-server/compare/6.2.1...6.2.2) (2023-09-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Pointer allows to access internal Parse Server classes and circumvent `beforeFind` query trigger; fixes security vulnerability [GHSA-fcv6-fg5r-jm9q](https://github.com/parse-community/parse-server/security/advisories/GHSA-fcv6-fg5r-jm9q) ([be4c7e2](https://github.com/parse-community/parse-server/commit/be4c7e23c63a2fb690685665cebed0de26be05c5))
|
||||
|
||||
## [6.2.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.2.1) (2023-06-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) ([#8674](https://github.com/parse-community/parse-server/issues/8674)) ([3dd99dd](https://github.com/parse-community/parse-server/commit/3dd99dd80e27e5e1d99b42844180546d90c7aa90))
|
||||
|
||||
# [6.2.0](https://github.com/parse-community/parse-server/compare/6.1.0...6.2.0) (2023-05-20)
|
||||
|
||||
|
||||
|
||||
+20
-18
@@ -1,8 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const CiVersionCheck = require('./CiVersionCheck');
|
||||
const mongoVersionList = require('mongodb-version-list');
|
||||
const allNodeVersions = require('all-node-versions');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
async function check() {
|
||||
// Run checks
|
||||
@@ -14,14 +13,16 @@ async function check() {
|
||||
* Check the MongoDB versions used in test environments.
|
||||
*/
|
||||
async function checkMongoDbVersions() {
|
||||
const releasedVersions = await new Promise((resolve, reject) => {
|
||||
mongoVersionList(function (error, versions) {
|
||||
let latestStableVersions = await new Promise((resolve, reject) => {
|
||||
exec('m ls', (error, stdout) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(versions);
|
||||
resolve(stdout.trim());
|
||||
});
|
||||
});
|
||||
latestStableVersions = latestStableVersions.split('\n').map(version => version.trim());
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'MongoDB',
|
||||
@@ -29,13 +30,14 @@ async function checkMongoDbVersions() {
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'MONGODB_VERSION',
|
||||
releasedVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.minor,
|
||||
releasedVersions: latestStableVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.patch,
|
||||
ignoreReleasedVersions: [
|
||||
'<4.0.0', // Versions reached their MongoDB end-of-life support date
|
||||
'~4.1.0', // Development release according to MongoDB support
|
||||
'~4.3.0', // Development release according to MongoDB support
|
||||
'~4.7.0', // Development release according to MongoDB support
|
||||
'<4.2.0', // These versions have reached their end-of-life support date
|
||||
'>=4.3.0 <5.0.0', // Unsupported rapid release versions
|
||||
'>=5.1.0 <6.0.0', // Unsupported rapid release versions
|
||||
'>=6.1.0 <7.0.0', // Unsupported rapid release versions
|
||||
'>=7.1.0 <8.0.0', // Unsupported rapid release versions
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
@@ -44,8 +46,9 @@ async function checkMongoDbVersions() {
|
||||
* Check the Nodejs versions used in test environments.
|
||||
*/
|
||||
async function checkNodeVersions() {
|
||||
const allVersions = await allNodeVersions();
|
||||
const releasedVersions = allVersions.versions;
|
||||
const allVersions = (await import('all-node-versions')).default;
|
||||
const { versions } = await allVersions();
|
||||
const nodeVersions = versions.map(version => version.node);
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'Node.js',
|
||||
@@ -53,13 +56,12 @@ async function checkNodeVersions() {
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'NODE_VERSION',
|
||||
releasedVersions,
|
||||
releasedVersions: nodeVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.minor,
|
||||
ignoreReleasedVersions: [
|
||||
'<12.0.0', // These versions have reached their end-of-life support date
|
||||
'>=13.0.0 <14.0.0', // These versions have reached their end-of-life support date
|
||||
'>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date
|
||||
'>=19.0.0', // These versions are not officially supported yet
|
||||
'<18.0.0', // These versions have reached their end-of-life support date
|
||||
'>=19.0.0 <20.0.0', // These versions have reached their end-of-life support date
|
||||
'>=21.0.0', // These versions are not officially supported yet
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Read package exclusion list from arguments
|
||||
exclusionList=("$@")
|
||||
|
||||
# Convert exclusion list to grep pattern
|
||||
exclusionPattern=$(printf "|%s" "${exclusionList[@]}")
|
||||
exclusionPattern=${exclusionPattern:1}
|
||||
|
||||
# Get list of all dev dependencies
|
||||
devDeps=$(jq -r '.devDependencies | keys | .[]' package.json)
|
||||
|
||||
# Filter out exclusion list
|
||||
depsToUninstall=$(echo "$devDeps" | grep -Ev "$exclusionPattern")
|
||||
|
||||
# If there are dependencies to uninstall then uninstall them
|
||||
if [ -n "$depsToUninstall" ]; then
|
||||
echo "Uninstalling dev dependencies: $depsToUninstall"
|
||||
npm uninstall $depsToUninstall
|
||||
else
|
||||
echo "No dev dependencies to uninstall"
|
||||
fi
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
"template": "./node_modules/clean-jsdoc-theme",
|
||||
"theme_opts": {
|
||||
"default_theme": "dark",
|
||||
"title": "Parse Server",
|
||||
"title": "<img src='https://raw.githubusercontent.com/parse-community/parse-server/alpha/.github/parse-server-logo.png' class='logo'/>",
|
||||
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
|
||||
}
|
||||
},
|
||||
|
||||
Generated
+16733
-16092
File diff suppressed because it is too large
Load Diff
+59
-58
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "6.3.0-beta.1",
|
||||
"version": "7.2.0",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -19,92 +19,92 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@apollo/server": "4.10.1",
|
||||
"@babel/eslint-parser": "7.21.8",
|
||||
"@graphql-tools/merge": "8.4.1",
|
||||
"@graphql-tools/schema": "9.0.4",
|
||||
"@graphql-tools/merge": "9.0.3",
|
||||
"@graphql-tools/schema": "10.0.3",
|
||||
"@graphql-tools/utils": "8.12.0",
|
||||
"@graphql-yoga/node": "2.6.0",
|
||||
"@parse/fs-files-adapter": "1.2.2",
|
||||
"@parse/push-adapter": "4.1.3",
|
||||
"@parse/fs-files-adapter": "3.0.0",
|
||||
"@parse/push-adapter": "6.4.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.20.2",
|
||||
"commander": "10.0.1",
|
||||
"commander": "12.0.0",
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.18.2",
|
||||
"express-rate-limit": "6.7.0",
|
||||
"follow-redirects": "1.15.2",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
"graphql-relay": "0.10.0",
|
||||
"express": "4.19.2",
|
||||
"express-rate-limit": "6.11.2",
|
||||
"follow-redirects": "1.15.6",
|
||||
"graphql": "16.8.1",
|
||||
"graphql-list-fields": "2.0.4",
|
||||
"graphql-relay": "0.10.1",
|
||||
"graphql-tag": "2.12.6",
|
||||
"graphql-upload": "15.0.2",
|
||||
"intersect": "1.0.1",
|
||||
"ip-range-check": "0.2.0",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"jwks-rsa": "2.1.5",
|
||||
"ldapjs": "2.3.3",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"jwks-rsa": "3.1.0",
|
||||
"ldapjs": "3.0.7",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "9.1.1",
|
||||
"lru-cache": "10.2.2",
|
||||
"mime": "3.0.0",
|
||||
"mongodb": "4.10.0",
|
||||
"mongodb": "5.9.0",
|
||||
"mustache": "4.2.0",
|
||||
"parse": "4.1.0",
|
||||
"otpauth": "9.2.2",
|
||||
"parse": "5.3.0",
|
||||
"path-to-regexp": "6.2.1",
|
||||
"pg-monitor": "2.0.0",
|
||||
"pg-promise": "11.5.0",
|
||||
"pg-promise": "11.7.8",
|
||||
"pluralize": "8.0.0",
|
||||
"rate-limit-redis": "3.0.2",
|
||||
"redis": "4.6.6",
|
||||
"semver": "7.5.1",
|
||||
"rate-limit-redis": "4.2.0",
|
||||
"redis": "4.6.13",
|
||||
"semver": "7.6.0",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "9.0.0",
|
||||
"winston": "3.8.2",
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
"ws": "8.13.0"
|
||||
"uuid": "9.0.1",
|
||||
"winston": "3.12.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "1.9.1",
|
||||
"@apollo/client": "3.6.1",
|
||||
"@babel/cli": "7.10.0",
|
||||
"@babel/core": "7.20.2",
|
||||
"@actions/core": "1.10.1",
|
||||
"@apollo/client": "3.9.11",
|
||||
"@babel/cli": "7.23.9",
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.9.0",
|
||||
"@babel/preset-env": "7.10.0",
|
||||
"@saithodev/semantic-release-backmerge": "2.1.2",
|
||||
"@semantic-release/changelog": "5.0.1",
|
||||
"@semantic-release/commit-analyzer": "8.0.1",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
"@semantic-release/github": "7.2.3",
|
||||
"@semantic-release/npm": "7.1.3",
|
||||
"@semantic-release/release-notes-generator": "9.0.3",
|
||||
"all-node-versions": "11.3.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.23.3",
|
||||
"@babel/preset-env": "7.24.0",
|
||||
"@saithodev/semantic-release-backmerge": "4.0.1",
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/commit-analyzer": "13.0.0",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/github": "10.1.0",
|
||||
"@semantic-release/npm": "12.0.1",
|
||||
"@semantic-release/release-notes-generator": "14.0.1",
|
||||
"all-node-versions": "12.1.0",
|
||||
"apollo-upload-client": "17.0.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"clean-jsdoc-theme": "4.2.7",
|
||||
"clean-jsdoc-theme": "4.2.18",
|
||||
"cross-env": "7.0.2",
|
||||
"deep-diff": "1.0.2",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-plugin-flowtype": "8.0.3",
|
||||
"flow-bin": "0.119.1",
|
||||
"form-data": "3.0.0",
|
||||
"form-data": "4.0.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"husky": "4.3.8",
|
||||
"husky": "9.0.11",
|
||||
"jasmine": "3.5.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "3.6.3",
|
||||
"jsdoc": "4.0.2",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "10.2.3",
|
||||
"madge": "5.0.1",
|
||||
"m": "1.9.0",
|
||||
"madge": "6.1.0",
|
||||
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
|
||||
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
|
||||
"mongodb-runner": "4.8.1",
|
||||
"mongodb-version-list": "1.0.0",
|
||||
"node-abort-controller": "3.0.1",
|
||||
"mongodb-runner": "5.5.4",
|
||||
"node-abort-controller": "3.1.1",
|
||||
"node-fetch": "3.2.10",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "17.4.6",
|
||||
"semantic-release": "24.0.0",
|
||||
"yaml": "1.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -117,18 +117,19 @@
|
||||
"lint-fix": "eslint --fix --cache ./",
|
||||
"build": "babel src/ -d lib/ --copy-files",
|
||||
"watch": "babel --watch src/ -d lib/ --copy-files",
|
||||
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start",
|
||||
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
|
||||
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion",
|
||||
"test:mongodb:4.2.19": "npm run test:mongodb --dbversion=4.2.19",
|
||||
"test:mongodb:4.4.13": "npm run test:mongodb --dbversion=4.4.13",
|
||||
"test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2",
|
||||
"test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2",
|
||||
"posttest:mongodb": "mongodb-runner stop",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start",
|
||||
"test:mongodb:7.0.1": "npm run test:mongodb --dbversion=7.0.1",
|
||||
"test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test": "npm run testonly",
|
||||
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner stop",
|
||||
"posttest": "cross-env mongodb-runner stop --all",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
|
||||
@@ -137,13 +138,13 @@
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.21.0 <17 || >=18 <19"
|
||||
"node": ">=18.0.0 <21"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@node-rs/bcrypt": "1.1.0"
|
||||
"@node-rs/bcrypt": "1.10.4"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
const pkg = require('./package.json');
|
||||
|
||||
const version = parseFloat(process.version.substr(1));
|
||||
const version = parseFloat(process.version.substring(1));
|
||||
const minimum = parseFloat(pkg.engines.node.match(/\d+/g).join('.'));
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
+21
-9
@@ -1,10 +1,17 @@
|
||||
#!/bin/sh -e
|
||||
set -x
|
||||
# GITHUB_ACTIONS=true SOURCE_TAG=test ./release_docs.sh
|
||||
|
||||
if [ "${GITHUB_ACTIONS}" = "" ];
|
||||
then
|
||||
echo "Cannot release docs without GITHUB_ACTIONS set"
|
||||
exit 0;
|
||||
fi
|
||||
if [ "${SOURCE_TAG}" = "" ];
|
||||
then
|
||||
echo "Cannot release docs without SOURCE_TAG set"
|
||||
exit 0;
|
||||
fi
|
||||
REPO="https://github.com/parse-community/parse-server"
|
||||
|
||||
rm -rf docs
|
||||
@@ -13,17 +20,22 @@ cd docs
|
||||
git pull origin gh-pages
|
||||
cd ..
|
||||
|
||||
DEST="master"
|
||||
RELEASE="release"
|
||||
VERSION="${SOURCE_TAG}"
|
||||
|
||||
if [ "${SOURCE_TAG}" != "" ];
|
||||
then
|
||||
DEST="${SOURCE_TAG}"
|
||||
# change the default page to the latest
|
||||
echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${DEST}'>" > "docs/api/index.html"
|
||||
fi
|
||||
# change the default page to the latest
|
||||
echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${VERSION}'>" > "docs/api/index.html"
|
||||
|
||||
npm run definitions
|
||||
npm run docs
|
||||
|
||||
mkdir -p "docs/api/${DEST}"
|
||||
cp -R out/* "docs/api/${DEST}"
|
||||
mkdir -p "docs/api/${RELEASE}"
|
||||
cp -R out/* "docs/api/${RELEASE}"
|
||||
|
||||
mkdir -p "docs/api/${VERSION}"
|
||||
cp -R out/* "docs/api/${VERSION}"
|
||||
|
||||
# Copy other resources
|
||||
RESOURCE_DIR=".github"
|
||||
mkdir -p "docs/${RESOURCE_DIR}"
|
||||
cp "./.github/parse-server-logo.png" "docs/${RESOURCE_DIR}/"
|
||||
|
||||
@@ -254,8 +254,34 @@ function inject(t, list) {
|
||||
if (action) {
|
||||
props.push(t.objectProperty(t.stringLiteral('action'), action));
|
||||
}
|
||||
|
||||
if (t.isGenericTypeAnnotation(elt)) {
|
||||
if (elt.typeAnnotation.id.name in nestedOptionEnvPrefix) {
|
||||
props.push(
|
||||
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elt.typeAnnotation.id.name))
|
||||
);
|
||||
}
|
||||
} else if (t.isArrayTypeAnnotation(elt)) {
|
||||
const elementType = elt.typeAnnotation.elementType;
|
||||
if (t.isGenericTypeAnnotation(elementType)) {
|
||||
if (elementType.id.name in nestedOptionEnvPrefix) {
|
||||
props.push(
|
||||
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elementType.id.name + '[]'))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (elt.defaultValue) {
|
||||
const parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (!parsedValue) {
|
||||
for (const type of elt.typeAnnotation.types) {
|
||||
elt.type = type.type;
|
||||
parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (parsedValue) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsedValue) {
|
||||
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"equal": true,
|
||||
"expectAsync": true,
|
||||
"notEqual": true,
|
||||
"it_id": true,
|
||||
"it_only_db": true,
|
||||
"it_only_mongodb_version": true,
|
||||
"it_only_postgres_version": true,
|
||||
@@ -27,6 +28,7 @@
|
||||
"fit_exclude_node_version": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"fdescribe_only_db": true,
|
||||
"describe_only": true,
|
||||
"on_db": true,
|
||||
"defaultConfiguration": true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter;
|
||||
const { loadAdapter, loadModule } = require('../lib/Adapters/AdapterLoader');
|
||||
const FilesAdapter = require('@parse/fs-files-adapter').default;
|
||||
const ParsePushAdapter = require('@parse/push-adapter').default;
|
||||
const MockFilesAdapter = require('mock-files-adapter');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
@@ -103,19 +102,19 @@ describe('AdapterLoader', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load push adapter from options', done => {
|
||||
it('should load push adapter from options', async () => {
|
||||
const options = {
|
||||
android: {
|
||||
senderId: 'yolo',
|
||||
apiKey: 'yolo',
|
||||
},
|
||||
};
|
||||
const ParsePushAdapter = await loadModule('@parse/push-adapter');
|
||||
expect(() => {
|
||||
const adapter = loadAdapter(undefined, ParsePushAdapter, options);
|
||||
expect(adapter.constructor).toBe(ParsePushAdapter);
|
||||
expect(adapter).not.toBe(undefined);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load custom push adapter from string (#3544)', done => {
|
||||
@@ -142,6 +141,25 @@ describe('AdapterLoader', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should load custom database adapter from config', done => {
|
||||
const adapterPath = require('path').resolve('./spec/support/MockDatabaseAdapter');
|
||||
const options = {
|
||||
databaseURI: 'oracledb://user:password@localhost:1521/freepdb1',
|
||||
collectionPrefix: '',
|
||||
};
|
||||
const databaseAdapterOptions = {
|
||||
adapter: adapterPath,
|
||||
options,
|
||||
};
|
||||
expect(() => {
|
||||
const databaseAdapter = loadAdapter(databaseAdapterOptions);
|
||||
expect(databaseAdapter).not.toBe(undefined);
|
||||
expect(databaseAdapter.options).toEqual(options);
|
||||
expect(databaseAdapter.getDatabaseURI()).toEqual(options.databaseURI);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load file adapter from direct passing', done => {
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
const mockFilesAdapter = new MockFilesAdapter('key', 'secret', 'bucket');
|
||||
|
||||
@@ -339,11 +339,12 @@ describe('AudiencesRouter', () => {
|
||||
)
|
||||
.then(result => {
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
database
|
||||
.collection('test__Audience')
|
||||
.find({ _id: audience.objectId })
|
||||
.toArray((error, rows) => {
|
||||
expect(error).toEqual(undefined);
|
||||
.toArray()
|
||||
.then(rows => {
|
||||
expect(rows[0]['times_used']).toEqual(1);
|
||||
expect(rows[0]['_last_used']).toEqual(now);
|
||||
Parse._request(
|
||||
@@ -362,6 +363,9 @@ describe('AudiencesRouter', () => {
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+21
-11
@@ -81,17 +81,6 @@ describe('Auth', () => {
|
||||
.then(roles => expect(roles).toEqual([]))
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should properly handle bcrypt upgrade', done => {
|
||||
const bcryptOriginal = require('bcrypt-nodejs');
|
||||
const bcryptNew = require('bcryptjs');
|
||||
bcryptOriginal.hash('my1Long:password', null, null, function (err, res) {
|
||||
bcryptNew.compare('my1Long:password', res, function (err, res) {
|
||||
expect(res).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can use extendSessionOnUse', async () => {
|
||||
@@ -271,3 +260,24 @@ describe('Auth', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('extendSessionOnUse', () => {
|
||||
it(`shouldUpdateSessionExpiry()`, async () => {
|
||||
const { shouldUpdateSessionExpiry } = require('../lib/Auth');
|
||||
let update = new Date(Date.now() - 86410 * 1000);
|
||||
|
||||
const res = shouldUpdateSessionExpiry(
|
||||
{ sessionLength: 86460 },
|
||||
{ updatedAt: update }
|
||||
);
|
||||
|
||||
update = new Date(Date.now() - 43210 * 1000);
|
||||
const res2 = shouldUpdateSessionExpiry(
|
||||
{ sessionLength: 86460 },
|
||||
{ updatedAt: update }
|
||||
);
|
||||
|
||||
expect(res).toBe(true);
|
||||
expect(res2).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+392
-150
@@ -589,20 +589,6 @@ describe('AuthenticationProviders', function () {
|
||||
new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, 'This authentication method is unsupported.')
|
||||
);
|
||||
});
|
||||
|
||||
it('can deprecate', async () => {
|
||||
await reconfigureServer();
|
||||
const Deprecator = require('../lib/Deprecator/Deprecator');
|
||||
const spy = spyOn(Deprecator, 'logRuntimeDeprecation').and.callFake(() => {});
|
||||
const provider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
await Parse.User._logInWith('myoauth');
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
usage: 'Using the authentication adapter "myoauth" without explicitly enabling it',
|
||||
solution:
|
||||
'Enable the authentication adapter by setting the Parse Server option "auth.myoauth.enabled: true".',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('instagram auth adapter', () => {
|
||||
@@ -1454,7 +1440,6 @@ describe('oauth2 auth adapter', () => {
|
||||
describe('apple signin auth adapter', () => {
|
||||
const apple = require('../lib/Adapters/Auth/apple');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
it('(using client id as string) should throw error with missing id_token', async () => {
|
||||
@@ -1512,12 +1497,10 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -1529,11 +1512,9 @@ describe('apple signin auth adapter', () => {
|
||||
|
||||
it('should not verify invalid id_token', async () => {
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
|
||||
try {
|
||||
await apple.validateAuthData(
|
||||
@@ -1566,11 +1547,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
@@ -1588,11 +1567,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
@@ -1610,11 +1587,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
@@ -1630,11 +1605,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1658,11 +1631,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1687,11 +1658,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1759,11 +1728,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2025,7 +1992,6 @@ describe('microsoft graph auth adapter', () => {
|
||||
describe('facebook limited auth adapter', () => {
|
||||
const facebook = require('../lib/Adapters/Auth/facebook');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
// TODO: figure out a way to run this test alongside facebook classic tests
|
||||
@@ -2081,23 +2047,16 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('should use algorithm from key header to verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -2108,17 +2067,10 @@ describe('facebook limited auth adapter', () => {
|
||||
});
|
||||
|
||||
it('should not verify invalid id_token', async () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
@@ -2145,22 +2097,15 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('(using client id as string) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
@@ -2172,22 +2117,15 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('(using client id as array) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
@@ -2199,22 +2137,15 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('(using client id as array with multiple items) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
@@ -2229,17 +2160,10 @@ describe('facebook limited auth adapter', () => {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2250,7 +2174,7 @@ describe('facebook limited auth adapter', () => {
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -2262,17 +2186,10 @@ describe('facebook limited auth adapter', () => {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2286,7 +2203,7 @@ describe('facebook limited auth adapter', () => {
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -2296,17 +2213,10 @@ describe('facebook limited auth adapter', () => {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2320,7 +2230,7 @@ describe('facebook limited auth adapter', () => {
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -2378,21 +2288,14 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('should throw error with with invalid user id', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'invalid_client_id',
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2406,3 +2309,342 @@ describe('facebook limited auth adapter', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('OTP TOTP auth adatper', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mfa: {
|
||||
enabled: true,
|
||||
options: ['TOTP'],
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can enroll', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
const response = user.get('authDataResponse');
|
||||
expect(response.mfa).toBeDefined();
|
||||
expect(response.mfa.recovery).toBeDefined();
|
||||
expect(response.mfa.recovery.split(',').length).toEqual(2);
|
||||
await user.fetch();
|
||||
expect(user.get('authData').mfa).toEqual({ status: 'enabled' });
|
||||
});
|
||||
|
||||
it('can login with valid token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: totp.generate(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).then(res => res.data);
|
||||
expect(response.objectId).toEqual(user.id);
|
||||
expect(response.sessionToken).toBeDefined();
|
||||
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
|
||||
expect(Object.keys(response).sort()).toEqual(
|
||||
[
|
||||
'objectId',
|
||||
'username',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'authData',
|
||||
'ACL',
|
||||
'sessionToken',
|
||||
'authDataResponse',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('can change OTP with valid token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
const new_secret = new OTPAuth.Secret();
|
||||
const new_totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: new_secret,
|
||||
});
|
||||
const new_token = new_totp.generate();
|
||||
await user.save(
|
||||
{
|
||||
authData: { mfa: { secret: new_secret.base32, token: new_token, old: totp.generate() } },
|
||||
},
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('authData').mfa.secret).toEqual(new_secret.base32);
|
||||
});
|
||||
|
||||
it('cannot change OTP with invalid token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
const new_secret = new OTPAuth.Secret();
|
||||
const new_totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: new_secret,
|
||||
});
|
||||
const new_token = new_totp.generate();
|
||||
await expectAsync(
|
||||
user.save(
|
||||
{
|
||||
authData: { mfa: { secret: new_secret.base32, token: new_token, old: '123' } },
|
||||
},
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
)
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.OTHER_CAUSE, 'Invalid MFA token'));
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('authData').mfa.secret).toEqual(secret.base32);
|
||||
});
|
||||
|
||||
it('future logins require TOTP token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
|
||||
);
|
||||
});
|
||||
|
||||
it('future logins reject incorrect TOTP token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
await expectAsync(
|
||||
request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: 'abcd',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw e.data;
|
||||
})
|
||||
).toBeRejectedWith({ code: Parse.Error.SCRIPT_FAILED, error: 'Invalid MFA token' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('OTP SMS auth adatper', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
let code;
|
||||
let mobile;
|
||||
const mfa = {
|
||||
enabled: true,
|
||||
options: ['SMS'],
|
||||
sendSMS(smsCode, number) {
|
||||
expect(smsCode).toBeDefined();
|
||||
expect(number).toBeDefined();
|
||||
expect(smsCode.length).toEqual(6);
|
||||
code = smsCode;
|
||||
mobile = number;
|
||||
},
|
||||
digits: 6,
|
||||
period: 30,
|
||||
};
|
||||
beforeEach(async () => {
|
||||
code = '';
|
||||
mobile = '';
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mfa,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can enroll', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const sessionToken = user.getSessionToken();
|
||||
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
||||
await user.save({ authData: { mfa: { mobile: '+11111111111' } } }, { sessionToken });
|
||||
await user.fetch({ sessionToken });
|
||||
expect(user.get('authData')).toEqual({ mfa: { status: 'disabled' } });
|
||||
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
|
||||
await user.fetch({ useMasterKey: true });
|
||||
const authData = user.get('authData').mfa?.pending;
|
||||
expect(authData).toBeDefined();
|
||||
expect(authData['+11111111111']).toBeDefined();
|
||||
expect(Object.keys(authData['+11111111111'])).toEqual(['token', 'expiry']);
|
||||
|
||||
await user.save({ authData: { mfa: { mobile, token: code } } }, { sessionToken });
|
||||
await user.fetch({ sessionToken });
|
||||
expect(user.get('authData')).toEqual({ mfa: { status: 'enabled' } });
|
||||
});
|
||||
|
||||
it('future logins require SMS code', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
||||
await user.save(
|
||||
{ authData: { mfa: { mobile: '+11111111111' } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
await user.save(
|
||||
{ authData: { mfa: { mobile, token: code } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
spy.calls.reset();
|
||||
|
||||
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
|
||||
);
|
||||
const res = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: 'request',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).catch(e => e.data);
|
||||
expect(res).toEqual({ code: Parse.Error.SCRIPT_FAILED, error: 'Please enter the token' });
|
||||
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: code,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).then(res => res.data);
|
||||
expect(response.objectId).toEqual(user.id);
|
||||
expect(response.sessionToken).toBeDefined();
|
||||
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
|
||||
expect(Object.keys(response).sort()).toEqual(
|
||||
[
|
||||
'objectId',
|
||||
'username',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'authData',
|
||||
'ACL',
|
||||
'sessionToken',
|
||||
'authDataResponse',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('partially enrolled users can still login', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
await user.save({ authData: { mfa: { mobile: '+11111111111' } } });
|
||||
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
||||
await Parse.User.logIn('username', 'password');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -487,6 +487,33 @@ describe('Auth Adapter features', () => {
|
||||
expect(baseAdapter2.validateAuthData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not perform authData validation twice when data mutated', async () => {
|
||||
spyOn(baseAdapter, 'validateAuthData').and.resolveTo({});
|
||||
await reconfigureServer({
|
||||
auth: { baseAdapter },
|
||||
allowExpiredAuthDataToken: false,
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
|
||||
await user.save({
|
||||
authData: {
|
||||
baseAdapter: { id: 'baseAdapter', token: "sometoken1" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(baseAdapter.validateAuthData).toHaveBeenCalledTimes(1);
|
||||
|
||||
const user2 = new Parse.User();
|
||||
await user2.save({
|
||||
authData: {
|
||||
baseAdapter: { id: 'baseAdapter', token: "sometoken2" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(baseAdapter.validateAuthData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should require additional provider if configured', async () => {
|
||||
await reconfigureServer({
|
||||
auth: { baseAdapter, additionalAdapter },
|
||||
|
||||
@@ -302,4 +302,24 @@ describe('execution', () => {
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it('can start Parse Server with auth via CLI', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
['--databaseURI', databaseURI, './spec/configs/CLIConfigAuth.json'],
|
||||
{ env }
|
||||
);
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
if (data.includes('parse-server running on')) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
childProcess.stderr.on('data', data => {
|
||||
data = data.toString();
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+151
-67
@@ -103,6 +103,14 @@ describe('Cloud Code', () => {
|
||||
expect(currentConfig.silent).toBeFalse();
|
||||
});
|
||||
|
||||
it('can get curent version', () => {
|
||||
const version = require('../package.json').version;
|
||||
const currentConfig = Config.get('test');
|
||||
expect(Parse.Server.version).toBeDefined();
|
||||
expect(currentConfig.version).toBeDefined();
|
||||
expect(Parse.Server.version).toEqual(version);
|
||||
});
|
||||
|
||||
it('show warning on duplicate cloud functions', done => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'warn').and.callFake(() => {});
|
||||
@@ -1353,7 +1361,28 @@ describe('Cloud Code', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not encode Parse Objects', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: false });
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
user.set('deleted', false);
|
||||
await user.signUp();
|
||||
Parse.Cloud.define(
|
||||
'deleteAccount',
|
||||
async req => {
|
||||
expect(req.params.object instanceof Parse.Object).not.toBeTrue();
|
||||
return 'Object deleted';
|
||||
},
|
||||
{
|
||||
requireMaster: true,
|
||||
}
|
||||
);
|
||||
await Parse.Cloud.run('deleteAccount', { object: user.toPointer() }, { useMasterKey: true });
|
||||
});
|
||||
|
||||
it('allow cloud to encode Parse Objects', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: true });
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
@@ -2370,6 +2399,56 @@ describe('beforeFind hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sets correct beforeFind trigger isGet parameter for Parse.Object.fetch request', async () => {
|
||||
const hook = {
|
||||
method: req => {
|
||||
expect(req.isGet).toEqual(true);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'method').and.callThrough();
|
||||
Parse.Cloud.beforeFind('MyObject', hook.method);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
await obj.save();
|
||||
const getObj = await obj.fetch();
|
||||
expect(getObj).toBeInstanceOf(Parse.Object);
|
||||
expect(hook.method).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sets correct beforeFind trigger isGet parameter for Parse.Query.get request', async () => {
|
||||
const hook = {
|
||||
method: req => {
|
||||
expect(req.isGet).toEqual(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'method').and.callThrough();
|
||||
Parse.Cloud.beforeFind('MyObject', hook.method);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
await obj.save();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const getObj = await query.get(obj.id);
|
||||
expect(getObj).toBeInstanceOf(Parse.Object);
|
||||
expect(hook.method).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sets correct beforeFind trigger isGet parameter for Parse.Query.find request', async () => {
|
||||
const hook = {
|
||||
method: req => {
|
||||
expect(req.isGet).toEqual(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'method').and.callThrough();
|
||||
Parse.Cloud.beforeFind('MyObject', hook.method);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
await obj.save();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const findObjs = await query.find();
|
||||
expect(findObjs?.[0]).toBeInstanceOf(Parse.Object);
|
||||
expect(hook.method).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should have request headers', done => {
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
expect(req.headers).toBeDefined();
|
||||
@@ -2403,6 +2482,60 @@ describe('beforeFind hooks', () => {
|
||||
})
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should run beforeFind on pointers and array of pointers from an object', async () => {
|
||||
const obj1 = new Parse.Object('TestObject');
|
||||
const obj2 = new Parse.Object('TestObject2');
|
||||
const obj3 = new Parse.Object('TestObject');
|
||||
obj2.set('aField', 'aFieldValue');
|
||||
await obj2.save();
|
||||
obj1.set('pointerField', obj2);
|
||||
obj3.set('pointerFieldArray', [obj2]);
|
||||
await obj1.save();
|
||||
await obj3.save();
|
||||
const spy = jasmine.createSpy('beforeFindSpy');
|
||||
Parse.Cloud.beforeFind('TestObject2', spy);
|
||||
const query = new Parse.Query('TestObject');
|
||||
await query.get(obj1.id);
|
||||
// Pointer not included in query so we don't expect beforeFind to be called
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
const query2 = new Parse.Query('TestObject');
|
||||
query2.include('pointerField');
|
||||
const res = await query2.get(obj1.id);
|
||||
expect(res.get('pointerField').get('aField')).toBe('aFieldValue');
|
||||
// Pointer included in query so we expect beforeFind to be called
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const query3 = new Parse.Query('TestObject');
|
||||
query3.include('pointerFieldArray');
|
||||
const res2 = await query3.get(obj3.id);
|
||||
expect(res2.get('pointerFieldArray')[0].get('aField')).toBe('aFieldValue');
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should have access to context in include query in beforeFind hook', async () => {
|
||||
let beforeFindTestObjectCalled = false;
|
||||
let beforeFindTestObject2Called = false;
|
||||
const obj1 = new Parse.Object('TestObject');
|
||||
const obj2 = new Parse.Object('TestObject2');
|
||||
obj2.set('aField', 'aFieldValue');
|
||||
await obj2.save();
|
||||
obj1.set('pointerField', obj2);
|
||||
await obj1.save();
|
||||
Parse.Cloud.beforeFind('TestObject', req => {
|
||||
expect(req.context).toBeDefined();
|
||||
expect(req.context.a).toEqual('a');
|
||||
beforeFindTestObjectCalled = true;
|
||||
});
|
||||
Parse.Cloud.beforeFind('TestObject2', req => {
|
||||
expect(req.context).toBeDefined();
|
||||
expect(req.context.a).toEqual('a');
|
||||
beforeFindTestObject2Called = true;
|
||||
});
|
||||
const query = new Parse.Query('TestObject');
|
||||
await query.include('pointerField').find({ context: { a: 'a' } });
|
||||
expect(beforeFindTestObjectCalled).toBeTrue();
|
||||
expect(beforeFindTestObject2Called).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind hooks', () => {
|
||||
@@ -3220,7 +3353,7 @@ describe('beforeLogin hook', () => {
|
||||
expect(req.headers).toBeDefined();
|
||||
expect(req.ip).toBeDefined();
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.context).toBeUndefined();
|
||||
expect(req.context).toBeDefined();
|
||||
});
|
||||
|
||||
await Parse.User.signUp('tupac', 'shakur');
|
||||
@@ -3337,7 +3470,7 @@ describe('afterLogin hook', () => {
|
||||
expect(req.headers).toBeDefined();
|
||||
expect(req.ip).toBeDefined();
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.context).toBeUndefined();
|
||||
expect(req.context).toBeDefined();
|
||||
});
|
||||
|
||||
await Parse.User.signUp('testuser', 'p@ssword');
|
||||
@@ -3503,7 +3636,7 @@ describe('afterLogin hook', () => {
|
||||
});
|
||||
|
||||
describe('saveFile hooks', () => {
|
||||
it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => {
|
||||
it('beforeSave(Parse.File) should return file that is already saved and not save anything to files adapter', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
@@ -3519,7 +3652,7 @@ describe('saveFile hooks', () => {
|
||||
expect(createFileSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('beforeSaveFile should throw error', async () => {
|
||||
it('beforeSave(Parse.File) should throw error', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw new Parse.Error(400, 'some-error-message');
|
||||
@@ -3532,7 +3665,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('beforeSaveFile should change values of uploaded file by editing fileObject directly', async () => {
|
||||
it('beforeSave(Parse.File) should change values of uploaded file by editing fileObject directly', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
@@ -3561,7 +3694,7 @@ describe('saveFile hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('beforeSaveFile should change values by returning new fileObject', async () => {
|
||||
it('beforeSave(Parse.File) should change values by returning new fileObject', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
@@ -3595,7 +3728,7 @@ describe('saveFile hooks', () => {
|
||||
expect(file._name.indexOf(expectedFileName)).toBe(file._name.length - expectedFileName.length);
|
||||
});
|
||||
|
||||
it('beforeSaveFile should contain metadata and tags saved from client', async () => {
|
||||
it('beforeSave(Parse.File) should contain metadata and tags saved from client', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
@@ -3623,7 +3756,7 @@ describe('saveFile hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('beforeSaveFile should return same file data with new file name', async () => {
|
||||
it('beforeSave(Parse.File) should return same file data with new file name', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const config = Config.get('test');
|
||||
config.filesController.options.preserveFileName = true;
|
||||
@@ -3638,7 +3771,7 @@ describe('saveFile hooks', () => {
|
||||
expect(result.name()).toBe('2020-04-01.txt');
|
||||
});
|
||||
|
||||
it('afterSaveFile should set fileSize to null if beforeSave returns an already saved file', async () => {
|
||||
it('afterSave(Parse.File) should set fileSize to null if beforeSave returns an already saved file', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, req => {
|
||||
@@ -3658,7 +3791,7 @@ describe('saveFile hooks', () => {
|
||||
expect(createFileSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('afterSaveFile should throw error', async () => {
|
||||
it('afterSave(Parse.File) should throw error', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.afterSave(Parse.File, async () => {
|
||||
throw new Parse.Error(400, 'some-error-message');
|
||||
@@ -3672,7 +3805,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('afterSaveFile should call with fileObject', async done => {
|
||||
it('afterSave(Parse.File) should call with fileObject', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
req.file.setTags({ tagA: 'some-tag' });
|
||||
@@ -3688,7 +3821,7 @@ describe('saveFile hooks', () => {
|
||||
await file.save({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('afterSaveFile should change fileSize when file data changes', async done => {
|
||||
it('afterSave(Parse.File) should change fileSize when file data changes', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
expect(req.fileSize).toBe(3);
|
||||
@@ -3705,7 +3838,7 @@ describe('saveFile hooks', () => {
|
||||
await file.save({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeDeleteFile should call with fileObject', async () => {
|
||||
it('beforeDelete(Parse.File) should call with fileObject', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDelete(Parse.File, req => {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
@@ -3717,7 +3850,7 @@ describe('saveFile hooks', () => {
|
||||
await file.destroy({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeDeleteFile should throw error', async done => {
|
||||
it('beforeDelete(Parse.File) should throw error', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDelete(Parse.File, () => {
|
||||
throw new Error('some error message');
|
||||
@@ -3731,7 +3864,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('afterDeleteFile should call with fileObject', async done => {
|
||||
it('afterDelete(Parse.File) should call with fileObject', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDelete(Parse.File, req => {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
@@ -3748,7 +3881,7 @@ describe('saveFile hooks', () => {
|
||||
await file.destroy({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeSaveFile should not change file if nothing is returned', async () => {
|
||||
it('beforeSave(Parse.File) should not change file if nothing is returned', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
return;
|
||||
@@ -3758,7 +3891,7 @@ describe('saveFile hooks', () => {
|
||||
expect(result).toBe(file);
|
||||
});
|
||||
|
||||
it('throw custom error from beforeSaveFile', async done => {
|
||||
it('throw custom error from beforeSave(Parse.File) ', async done => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
|
||||
});
|
||||
@@ -3772,7 +3905,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('throw empty error from beforeSaveFile', async done => {
|
||||
it('throw empty error from beforeSave(Parse.File)', async done => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw null;
|
||||
});
|
||||
@@ -3785,55 +3918,6 @@ describe('saveFile hooks', () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('legacy hooks', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
const triggers = {
|
||||
beforeSaveFile(req) {
|
||||
req.file.setTags({ tagA: 'some-tag' });
|
||||
req.file.setMetadata({ foo: 'bar' });
|
||||
expect(req.triggerName).toEqual('beforeSave');
|
||||
expect(req.master).toBe(true);
|
||||
},
|
||||
afterSaveFile(req) {
|
||||
expect(req.master).toBe(true);
|
||||
expect(req.file._tags).toEqual({ tagA: 'some-tag' });
|
||||
expect(req.file._metadata).toEqual({ foo: 'bar' });
|
||||
},
|
||||
beforeDeleteFile(req) {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file._name).toEqual('popeye.txt');
|
||||
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
|
||||
expect(req.fileSize).toBe(null);
|
||||
},
|
||||
afterDeleteFile(req) {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file._name).toEqual('popeye.txt');
|
||||
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
|
||||
},
|
||||
};
|
||||
|
||||
for (const key in triggers) {
|
||||
spyOn(triggers, key).and.callThrough();
|
||||
Parse.Cloud[key](triggers[key]);
|
||||
}
|
||||
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
await new Parse.File('popeye.txt', [1, 2, 3], 'text/plain').destroy({ useMasterKey: true });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
for (const key in triggers) {
|
||||
expect(triggers[key]).toHaveBeenCalled();
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
`DeprecationWarning: Parse.Cloud.${key} is deprecated and will be removed in a future version. Use Parse.Cloud.${key.replace(
|
||||
'File',
|
||||
''
|
||||
)}(Parse.File, (request) => {})`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
|
||||
@@ -15,6 +15,13 @@ describe('Cloud Code Logger', () => {
|
||||
// useful to flip to false for fine tuning :).
|
||||
silent: true,
|
||||
logLevel: undefined,
|
||||
logLevels: {
|
||||
cloudFunctionError: 'error',
|
||||
cloudFunctionSuccess: 'info',
|
||||
triggerAfter: 'info',
|
||||
triggerBeforeError: 'error',
|
||||
triggerBeforeSuccess: 'info',
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.User.signUp('tester', 'abc')
|
||||
@@ -334,4 +341,53 @@ describe('Cloud Code Logger', () => {
|
||||
expect(args[0]).toBe('Parse error: ');
|
||||
expect(args[1].message).toBe('Object not found.');
|
||||
});
|
||||
|
||||
it('should log cloud function execution using the silent log level', async () => {
|
||||
await reconfigureServer({
|
||||
logLevels: {
|
||||
cloudFunctionSuccess: 'silent',
|
||||
cloudFunctionError: 'silent',
|
||||
},
|
||||
});
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
Parse.Cloud.define('bFunction', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
await Parse.Cloud.run('aFunction', { foo: 'bar' });
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
await expectAsync(Parse.Cloud.run('bFunction', { foo: 'bar' })).toBeRejected();
|
||||
// Not "Failed running cloud function message..."
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should log cloud function triggers using the silent log level', async () => {
|
||||
await reconfigureServer({
|
||||
logLevels: {
|
||||
triggerAfter: 'silent',
|
||||
triggerBeforeSuccess: 'silent',
|
||||
triggerBeforeError: 'silent',
|
||||
},
|
||||
});
|
||||
Parse.Cloud.beforeSave('TestClassError', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
Parse.Cloud.beforeSave('TestClass', () => {});
|
||||
Parse.Cloud.afterSave('TestClass', () => {});
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
const obj = new Parse.Object('TestClass');
|
||||
await obj.save();
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
const objError = new Parse.Object('TestClassError');
|
||||
await expectAsync(objError.save()).toBeRejected();
|
||||
// Not "beforeSave failed for TestClassError for user ..."
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const Config = require('../lib/Config');
|
||||
const DatabaseController = require('../lib/Controllers/DatabaseController.js');
|
||||
const validateQuery = DatabaseController._validateQuery;
|
||||
|
||||
@@ -361,6 +362,259 @@ describe('DatabaseController', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableCollationCaseComparison', () => {
|
||||
const dummyStorageAdapter = {
|
||||
find: () => Promise.resolve([]),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () => Promise.resolve([]),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Config.get(Parse.applicationId).schemaCache.clear();
|
||||
});
|
||||
|
||||
it('should force caseInsensitive to false with enableCollationCaseComparison option', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
enableCollationCaseComparison: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'find');
|
||||
spy.and.callThrough();
|
||||
await databaseController.find('SomeClass', {}, { caseInsensitive: true });
|
||||
expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(false);
|
||||
});
|
||||
|
||||
it('should support caseInsensitive without enableCollationCaseComparison option', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'find');
|
||||
spy.and.callThrough();
|
||||
await databaseController.find('_User', {}, { caseInsensitive: true });
|
||||
expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(true);
|
||||
});
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should create insensitive indexes without enableCollationCaseComparison',
|
||||
async () => {
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonFalse',
|
||||
databaseAdapter: undefined,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
await user.save({
|
||||
username: 'example',
|
||||
password: 'password',
|
||||
email: 'example@example.com',
|
||||
});
|
||||
const schemas = await Parse.Schema.all();
|
||||
const UserSchema = schemas.find(({ className }) => className === '_User');
|
||||
expect(UserSchema.indexes).toEqual({
|
||||
_id_: { _id: 1 },
|
||||
username_1: { username: 1 },
|
||||
case_insensitive_username: { username: 1 },
|
||||
case_insensitive_email: { email: 1 },
|
||||
email_1: { email: 1 },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should not create insensitive indexes with enableCollationCaseComparison',
|
||||
async () => {
|
||||
await reconfigureServer({
|
||||
enableCollationCaseComparison: true,
|
||||
databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonTrue',
|
||||
databaseAdapter: undefined,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
await user.save({
|
||||
username: 'example',
|
||||
password: 'password',
|
||||
email: 'example@example.com',
|
||||
});
|
||||
const schemas = await Parse.Schema.all();
|
||||
const UserSchema = schemas.find(({ className }) => className === '_User');
|
||||
expect(UserSchema.indexes).toEqual({
|
||||
_id_: { _id: 1 },
|
||||
username_1: { username: 1 },
|
||||
email_1: { email: 1 },
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('convertEmailToLowercase', () => {
|
||||
const dummyStorageAdapter = {
|
||||
createObject: () => Promise.resolve({ ops: [{}] }),
|
||||
findOneAndUpdate: () => Promise.resolve({}),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
className: '_User',
|
||||
fields: { email: 'String' },
|
||||
indexes: {},
|
||||
classLevelPermissions: { protectedFields: {} },
|
||||
},
|
||||
]),
|
||||
};
|
||||
const dates = {
|
||||
createdAt: { iso: undefined, __type: 'Date' },
|
||||
updatedAt: { iso: undefined, __type: 'Date' },
|
||||
};
|
||||
|
||||
it('should not transform email to lower case without convertEmailToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform email to lower case with convertEmailToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertEmailToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
email: 'example@example.com',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transform email to lower case without convertEmailToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform email to lower case with convertEmailToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertEmailToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
email: 'example@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not find a case insensitive user by email with convertEmailToLowercase', async () => {
|
||||
await reconfigureServer({ convertEmailToLowercase: true });
|
||||
const user = new Parse.User();
|
||||
await user.save({ username: 'EXAMPLE', email: 'EXAMPLE@EXAMPLE.COM', password: 'password' });
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.equalTo('email', 'EXAMPLE@EXAMPLE.COM');
|
||||
const result = await query.find({ useMasterKey: true });
|
||||
expect(result.length).toEqual(0);
|
||||
|
||||
const query2 = new Parse.Query(Parse.User);
|
||||
query2.equalTo('email', 'example@example.com');
|
||||
const result2 = await query2.find({ useMasterKey: true });
|
||||
expect(result2.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertUsernameToLowercase', () => {
|
||||
const dummyStorageAdapter = {
|
||||
createObject: () => Promise.resolve({ ops: [{}] }),
|
||||
findOneAndUpdate: () => Promise.resolve({}),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
className: '_User',
|
||||
fields: { username: 'String' },
|
||||
indexes: {},
|
||||
classLevelPermissions: { protectedFields: {} },
|
||||
},
|
||||
]),
|
||||
};
|
||||
const dates = {
|
||||
createdAt: { iso: undefined, __type: 'Date' },
|
||||
updatedAt: { iso: undefined, __type: 'Date' },
|
||||
};
|
||||
|
||||
it('should not transform username to lower case without convertUsernameToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
username: 'EXAMPLE',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
username: 'EXAMPLE',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform username to lower case with convertUsernameToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertUsernameToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
username: 'EXAMPLE',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
username: 'example',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transform username to lower case without convertUsernameToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
username: 'EXAMPLE',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform username to lower case with convertUsernameToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertUsernameToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
username: 'example',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not find a case insensitive user by username with convertUsernameToLowercase', async () => {
|
||||
await reconfigureServer({ convertUsernameToLowercase: true });
|
||||
const user = new Parse.User();
|
||||
await user.save({ username: 'EXAMPLE', password: 'password' });
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.equalTo('username', 'EXAMPLE');
|
||||
const result = await query.find({ useMasterKey: true });
|
||||
expect(result.length).toEqual(0);
|
||||
|
||||
const query2 = new Parse.Query(Parse.User);
|
||||
query2.equalTo('username', 'example');
|
||||
const result2 = await query2.find({ useMasterKey: true });
|
||||
expect(result2.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildCLP(pointerNames) {
|
||||
|
||||
@@ -554,7 +554,7 @@ describe('DefinedSchemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not delete automatically classes', async () => {
|
||||
it('should not delete classes automatically', async () => {
|
||||
await reconfigureServer({
|
||||
schema: { definitions: [{ className: '_User' }, { className: 'Test' }] },
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const Auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
const request = require('../lib/request');
|
||||
const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions');
|
||||
|
||||
describe('Email Verification Token Expiration: ', () => {
|
||||
it('show the invalid verification link page, if the user clicks on the verify email link after the email verify token expires', done => {
|
||||
@@ -126,6 +127,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -167,6 +169,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -214,6 +217,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -288,6 +292,223 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can conditionally send emails', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const verifyUserEmails = {
|
||||
method(req) {
|
||||
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
const beforeSave = {
|
||||
method(req) {
|
||||
req.object.set('emailVerified', true);
|
||||
},
|
||||
};
|
||||
const saveSpy = spyOn(beforeSave, 'method').and.callThrough();
|
||||
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.User, beforeSave.method);
|
||||
const user = new Parse.User();
|
||||
user.setUsername('sets_email_verify_token_expires_at');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const config = Config.get('test');
|
||||
const results = await config.database.find(
|
||||
'_User',
|
||||
{
|
||||
username: 'sets_email_verify_token_expires_at',
|
||||
},
|
||||
{},
|
||||
Auth.maintenance(config)
|
||||
);
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
const user_data = results[0];
|
||||
expect(typeof user_data).toBe('object');
|
||||
expect(user_data.emailVerified).toEqual(true);
|
||||
expect(user_data._email_verify_token).toBeUndefined();
|
||||
expect(user_data._email_verify_token_expires_at).toBeUndefined();
|
||||
expect(emailSpy).not.toHaveBeenCalled();
|
||||
expect(saveSpy).toHaveBeenCalled();
|
||||
expect(sendEmailOptions).toBeUndefined();
|
||||
expect(verifySpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can conditionally send emails and allow conditional login', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const verifyUserEmails = {
|
||||
method(req) {
|
||||
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
|
||||
if (req.object.get('username') === 'no_email') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
const user = new Parse.User();
|
||||
user.setUsername('no_email');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
expect(sendEmailOptions).toBeUndefined();
|
||||
expect(user.getSessionToken()).toBeDefined();
|
||||
expect(verifySpy).toHaveBeenCalledTimes(2);
|
||||
const user2 = new Parse.User();
|
||||
user2.setUsername('email');
|
||||
user2.setPassword('expiringToken');
|
||||
user2.set('email', 'user2@example.com');
|
||||
await user2.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(user2.getSessionToken()).toBeUndefined();
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
expect(verifySpy).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it('can conditionally send user email verification', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const sendVerificationEmail = {
|
||||
method(req) {
|
||||
expect(req.user).toBeDefined();
|
||||
expect(req.master).toBeDefined();
|
||||
return false;
|
||||
},
|
||||
};
|
||||
const sendSpy = spyOn(sendVerificationEmail, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
sendUserEmailVerification: sendVerificationEmail.method,
|
||||
});
|
||||
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
|
||||
const newUser = new Parse.User();
|
||||
newUser.setUsername('unsets_email_verify_token_expires_at');
|
||||
newUser.setPassword('expiringToken');
|
||||
newUser.set('email', 'user@example.com');
|
||||
await newUser.signUp();
|
||||
await Parse.User.requestEmailVerification('user@example.com');
|
||||
await jasmine.timeout();
|
||||
expect(sendSpy).toHaveBeenCalledTimes(2);
|
||||
expect(emailSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('provides full user object in email verification function on email and username change', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const sendVerificationEmail = {
|
||||
method(req) {
|
||||
expect(req.user).toBeDefined();
|
||||
expect(req.user.id).toBeDefined();
|
||||
expect(req.user.get('createdAt')).toBeDefined();
|
||||
expect(req.user.get('updatedAt')).toBeDefined();
|
||||
expect(req.master).toBeDefined();
|
||||
return false;
|
||||
},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
sendUserEmailVerification: sendVerificationEmail.method,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
user.setPassword('password');
|
||||
user.setUsername('new@example.com');
|
||||
user.setEmail('user@example.com');
|
||||
await user.save(null, { useMasterKey: true });
|
||||
|
||||
// Update email and username
|
||||
user.setUsername('new@example.com');
|
||||
user.setEmail('new@example.com');
|
||||
await user.save(null, { useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeSave options do not change existing behaviour', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
|
||||
const newUser = new Parse.User();
|
||||
newUser.setUsername('unsets_email_verify_token_expires_at');
|
||||
newUser.setPassword('expiringToken');
|
||||
newUser.set('email', 'user@parse.com');
|
||||
await newUser.signUp();
|
||||
await jasmine.timeout();
|
||||
const response = await request({
|
||||
url: sendEmailOptions.link,
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(response.status).toEqual(302);
|
||||
const config = Config.get('test');
|
||||
const results = await config.database.find('_User', {
|
||||
username: 'unsets_email_verify_token_expires_at',
|
||||
});
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
const user = results[0];
|
||||
expect(typeof user).toBe('object');
|
||||
expect(user.emailVerified).toEqual(true);
|
||||
expect(typeof user._email_verify_token).toBe('undefined');
|
||||
expect(typeof user._email_verify_token_expires_at).toBe('undefined');
|
||||
expect(emailSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => {
|
||||
const user = new Parse.User();
|
||||
let sendEmailOptions;
|
||||
@@ -311,6 +532,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -370,6 +592,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
return request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -587,6 +810,9 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toBe(200);
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
expect(sendVerificationEmailCallCount).toBe(2);
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
|
||||
@@ -616,6 +842,41 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('provides function arguments in verifyUserEmails on verificationEmailRequest', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.set('email', 'test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const verifyUserEmails = {
|
||||
method: async (params) => {
|
||||
expect(params.object).toBeInstanceOf(Parse.User);
|
||||
expect(params.ip).toBeDefined();
|
||||
expect(params.master).toBeDefined();
|
||||
expect(params.installationId).toBeDefined();
|
||||
expect(params.resendRequest).toBeTrue();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
await expectAsync(Parse.User.requestEmailVerification('test@example.com')).toBeResolved();
|
||||
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw with invalid emailVerifyTokenReuseIfValid', async done => {
|
||||
const sendEmailOptions = [];
|
||||
const emailAdapter = {
|
||||
@@ -683,7 +944,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
const config = Config.get('test');
|
||||
const [userBeforeRequest] = await config.database.find('_User', {
|
||||
username: 'resends_verification_token',
|
||||
});
|
||||
}, {}, Auth.maintenance(config));
|
||||
// store this user before we make our email request
|
||||
expect(sendVerificationEmailCallCount).toBe(1);
|
||||
await new Promise(resolve => {
|
||||
@@ -703,20 +964,21 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
await jasmine.timeout();
|
||||
expect(response.status).toBe(200);
|
||||
expect(sendVerificationEmailCallCount).toBe(2);
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
|
||||
const [userAfterRequest] = await config.database.find('_User', {
|
||||
username: 'resends_verification_token',
|
||||
});
|
||||
}, {}, Auth.maintenance(config));
|
||||
|
||||
// verify that our token & expiration has been changed for this new request
|
||||
// Verify that token & expiration haven't been changed for this new request
|
||||
expect(typeof userAfterRequest).toBe('object');
|
||||
expect(userBeforeRequest._email_verify_token).toBeDefined();
|
||||
expect(userBeforeRequest._email_verify_token).toEqual(userAfterRequest._email_verify_token);
|
||||
expect(userBeforeRequest._email_verify_token_expires_at).toEqual(
|
||||
userAfterRequest._email_verify_token_expires_at
|
||||
);
|
||||
expect(userBeforeRequest._email_verify_token_expires_at).toBeDefined();
|
||||
expect(userBeforeRequest._email_verify_token_expires_at).toEqual(userAfterRequest._email_verify_token_expires_at);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -745,6 +1007,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
return request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -983,6 +1246,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
|
||||
+130
-35
@@ -1,10 +1,19 @@
|
||||
const middlewares = require('../lib/middlewares');
|
||||
const AppCache = require('../lib/cache').AppCache;
|
||||
const { BlockList } = require('net');
|
||||
|
||||
const AppCachePut = (appId, config) =>
|
||||
AppCache.put(appId, {
|
||||
...config,
|
||||
maintenanceKeyIpsStore: new Map(),
|
||||
masterKeyIpsStore: new Map(),
|
||||
});
|
||||
|
||||
describe('middlewares', () => {
|
||||
let fakeReq, fakeRes;
|
||||
beforeEach(() => {
|
||||
fakeReq = {
|
||||
ip: '127.0.0.1',
|
||||
originalUrl: 'http://example.com/parse/',
|
||||
url: 'http://example.com/',
|
||||
body: {
|
||||
@@ -16,7 +25,7 @@ describe('middlewares', () => {
|
||||
},
|
||||
};
|
||||
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
|
||||
AppCache.put(fakeReq.body._ApplicationId, {});
|
||||
AppCachePut(fakeReq.body._ApplicationId, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -24,6 +33,9 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should use _ContentType if provided', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['127.0.0.1'],
|
||||
});
|
||||
expect(fakeReq.headers['content-type']).toEqual(undefined);
|
||||
const contentType = 'image/jpeg';
|
||||
fakeReq.body._ContentType = contentType;
|
||||
@@ -35,7 +47,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but no key supplied', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
@@ -44,7 +56,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
@@ -54,7 +66,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but different key is supplied', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
@@ -64,7 +76,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should succeed when any one of the configured keys supplied', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
clientKey: 'clientKey',
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
@@ -77,7 +89,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should succeed when client key supplied but empty', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
clientKey: '',
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
@@ -90,7 +102,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should succeed when no keys are configured and none supplied', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
@@ -117,7 +129,7 @@ describe('middlewares', () => {
|
||||
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
|
||||
);
|
||||
it(`it should pull ${bodyKey} into req.info`, done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
@@ -138,48 +150,55 @@ describe('middlewares', () => {
|
||||
it('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(false);
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Request using master key rejected as the request IP address '127.0.0.1' is not set in Parse Server option 'masterKeyIps'.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should not succeed if the ip does not belong to masterKeyIps list', async () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(false);
|
||||
});
|
||||
|
||||
it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => {
|
||||
it('should not succeed and log if the ip does not belong to maintenanceKeyIps list', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
maintenanceKey: 'masterKey',
|
||||
maintenanceKeyIps: ['10.0.0.0', '10.0.0.1'],
|
||||
});
|
||||
fakeReq.ip = '10.0.0.2';
|
||||
fakeReq.headers['x-parse-maintenance-key'] = 'masterKey';
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaintenance).toBe(false);
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Request using maintenance key rejected as the request IP address '10.0.0.2' is not set in Parse Server option 'maintenanceKeyIps'.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should succeed if the ip does belong to masterKeyIps list', async () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
@@ -190,7 +209,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
@@ -221,7 +240,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should set default Access-Control-Allow-Headers if allowHeaders are empty', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: undefined,
|
||||
});
|
||||
const headers = {};
|
||||
@@ -234,7 +253,7 @@ describe('middlewares', () => {
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
|
||||
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: [],
|
||||
});
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
@@ -242,7 +261,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should append custom headers to Access-Control-Allow-Headers if allowHeaders provided', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: ['Header-1', 'Header-2'],
|
||||
});
|
||||
const headers = {};
|
||||
@@ -258,7 +277,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should set default Access-Control-Allow-Origin if allowOrigin is empty', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: undefined,
|
||||
});
|
||||
const headers = {};
|
||||
@@ -273,7 +292,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should set custom origin to Access-Control-Allow-Origin if allowOrigin is provided', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: 'https://parseplatform.org/',
|
||||
});
|
||||
const headers = {};
|
||||
@@ -317,7 +336,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should use user provided on field userFromJWT', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
fakeReq.userFromJWT = 'fake-user';
|
||||
@@ -328,11 +347,87 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when upload file without x-parse-application-id in header', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
fakeReq.body = Buffer.from('fake-file');
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should match address', () => {
|
||||
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
|
||||
const anotherIpv6 = '::ffff:101.10.0.1';
|
||||
const ipv4 = '192.168.0.101';
|
||||
const localhostV6 = '::1';
|
||||
const localhostV62 = '::ffff:127.0.0.1';
|
||||
const localhostV4 = '127.0.0.1';
|
||||
|
||||
const v6 = [ipv6, anotherIpv6];
|
||||
v6.forEach(ip => {
|
||||
expect(middlewares.checkIp(ip, ['::/0'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ip, ['::'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ip, ['0.0.0.0'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ip, ['0.0.0.0/0'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ip, ['123.123.123.123'], new Map())).toBe(false);
|
||||
});
|
||||
|
||||
expect(middlewares.checkIp(ipv6, [anotherIpv6], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv6, [ipv6], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv6, ['2001:db8:85a3:0:0:8a2e:0:0/100'], new Map())).toBe(true);
|
||||
|
||||
expect(middlewares.checkIp(ipv4, ['::'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv4, ['::/0'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv4, ['0.0.0.0'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv4, ['0.0.0.0/0'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv4, ['123.123.123.123'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv4, [ipv4], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv4, ['192.168.0.0/24'], new Map())).toBe(true);
|
||||
|
||||
expect(middlewares.checkIp(localhostV4, ['::1'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(localhostV6, ['::1'], new Map())).toBe(true);
|
||||
// ::ffff:127.0.0.1 is a padded ipv4 address but not ::1
|
||||
expect(middlewares.checkIp(localhostV62, ['::1'], new Map())).toBe(false);
|
||||
// ::ffff:127.0.0.1 is a padded ipv4 address and is a match for 127.0.0.1
|
||||
expect(middlewares.checkIp(localhostV62, ['127.0.0.1'], new Map())).toBe(true);
|
||||
});
|
||||
|
||||
it('should match address with cache', () => {
|
||||
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
|
||||
const cache1 = new Map();
|
||||
const spyBlockListCheck = spyOn(BlockList.prototype, 'check').and.callThrough();
|
||||
expect(middlewares.checkIp(ipv6, ['::'], cache1)).toBe(true);
|
||||
expect(cache1.get('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe(undefined);
|
||||
expect(cache1.get('allowAllIpv6')).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(0);
|
||||
|
||||
const cache2 = new Map();
|
||||
expect(middlewares.checkIp('::1', ['::1'], cache2)).toBe(true);
|
||||
expect(cache2.get('::1')).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
expect(middlewares.checkIp('::1', ['::1'], cache2)).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
spyBlockListCheck.calls.reset();
|
||||
|
||||
const cache3 = new Map();
|
||||
expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache3)).toBe(true);
|
||||
expect(cache3.get('127.0.0.1')).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache3)).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
spyBlockListCheck.calls.reset();
|
||||
|
||||
const cache4 = new Map();
|
||||
const ranges = ['127.0.0.1', '192.168.0.0/24'];
|
||||
// should not cache negative match
|
||||
expect(middlewares.checkIp('123.123.123.123', ranges, cache4)).toBe(false);
|
||||
expect(cache4.get('123.123.123.123')).toBe(undefined);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
spyBlockListCheck.calls.reset();
|
||||
|
||||
// should not cache cidr
|
||||
expect(middlewares.checkIp('192.168.0.101', ranges, cache4)).toBe(true);
|
||||
expect(cache4.get('192.168.0.101')).toBe(undefined);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -254,6 +254,47 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
expect(obj.get('foo').test.date[0] instanceof Date).toBeTrue();
|
||||
});
|
||||
|
||||
it('upserts with $setOnInsert', async () => {
|
||||
const uuid = require('uuid');
|
||||
const uuid1 = uuid.v4();
|
||||
const uuid2 = uuid.v4();
|
||||
const schema = {
|
||||
className: 'MyClass',
|
||||
fields: {
|
||||
x: { type: 'Number' },
|
||||
count: { type: 'Number' },
|
||||
},
|
||||
classLevelPermissions: {},
|
||||
};
|
||||
|
||||
const myClassSchema = new Parse.Schema(schema.className);
|
||||
myClassSchema.setCLP(schema.classLevelPermissions);
|
||||
await myClassSchema.save();
|
||||
|
||||
const query = {
|
||||
x: 1,
|
||||
};
|
||||
const update = {
|
||||
objectId: {
|
||||
__op: 'SetOnInsert',
|
||||
amount: uuid1,
|
||||
},
|
||||
count: {
|
||||
__op: 'Increment',
|
||||
amount: 1,
|
||||
},
|
||||
};
|
||||
await Parse.Server.database.update('MyClass', query, update, { upsert: true });
|
||||
update.objectId.amount = uuid2;
|
||||
await Parse.Server.database.update('MyClass', query, update, { upsert: true });
|
||||
|
||||
const res = await Parse.Server.database.find(schema.className, {}, {});
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].objectId).toBe(uuid1);
|
||||
expect(res[0].count).toBe(2);
|
||||
expect(res[0].x).toBe(1);
|
||||
});
|
||||
|
||||
it('handles updating a single object with array, object date', done => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ describe('OAuth', function () {
|
||||
done();
|
||||
}
|
||||
|
||||
it('GET request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
xit('GET request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
@@ -105,7 +105,7 @@ describe('OAuth', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('POST request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
xit('POST request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
|
||||
@@ -749,6 +749,7 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -777,6 +778,7 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -830,6 +832,7 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -846,6 +849,8 @@ describe('Pages Router', () => {
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
await jasmine.timeout();
|
||||
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
@@ -1190,6 +1195,7 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkResponse = await request({
|
||||
@@ -1197,7 +1203,6 @@ describe('Pages Router', () => {
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(linkResponse.status).toBe(200);
|
||||
|
||||
const pagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(pagePath).toMatch(new RegExp(`\/${pages.emailVerificationSuccess.defaultFile}`));
|
||||
});
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
const request = require('../lib/request');
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const pushCompleted = async pushId => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
query.equalTo('objectId', pushId);
|
||||
let result = await query.first({ useMasterKey: true });
|
||||
while (!(result && result.get('status') === 'succeeded')) {
|
||||
await sleep(100);
|
||||
await jasmine.timeout();
|
||||
result = await query.first({ useMasterKey: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1267,6 +1267,26 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function query parameters with array of pointers', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: false });
|
||||
Parse.Cloud.define('echoParams', req => {
|
||||
return req.params;
|
||||
});
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/functions/echoParams',
|
||||
body: '{"arr": [{ "__type": "Pointer", "className": "PointerTest" }]}',
|
||||
});
|
||||
const res = response.data.result;
|
||||
expect(res.arr.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('can handle null params in cloud functions (regression test for #1742)', done => {
|
||||
Parse.Cloud.define('func', request => {
|
||||
expect(request.params.nullParam).toEqual(null);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
const Config = require('../lib/Config');
|
||||
const ParseServer = require('../lib/index').ParseServer;
|
||||
|
||||
describe('Config Keys', () => {
|
||||
const tests = [
|
||||
{
|
||||
name: 'Invalid Root Keys',
|
||||
options: { unknow: 'val', masterKeyIPs: '' },
|
||||
error: 'unknow, masterKeyIPs',
|
||||
},
|
||||
{ name: 'Invalid Schema Keys', options: { schema: { Strict: 'val' } }, error: 'schema.Strict' },
|
||||
{
|
||||
name: 'Invalid Pages Keys',
|
||||
options: { pages: { customUrls: { EmailVerificationSendFail: 'val' } } },
|
||||
error: 'pages.customUrls.EmailVerificationSendFail',
|
||||
},
|
||||
{
|
||||
name: 'Invalid LiveQueryServerOptions Keys',
|
||||
options: { liveQueryServerOptions: { MasterKey: 'value' } },
|
||||
error: 'liveQueryServerOptions.MasterKey',
|
||||
},
|
||||
{
|
||||
name: 'Invalid RateLimit Keys - Array Item',
|
||||
options: { rateLimit: [{ RequestPath: '' }, { RequestTimeWindow: '' }] },
|
||||
error: 'rateLimit[0].RequestPath, rateLimit[1].RequestTimeWindow',
|
||||
},
|
||||
];
|
||||
|
||||
tests.forEach(test => {
|
||||
it(test.name, async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
spyOn(Config, 'validateOptions').and.callFake(() => {});
|
||||
|
||||
new ParseServer({
|
||||
...defaultConfiguration,
|
||||
...test.options,
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith(`Invalid Option Keys Found: ${test.error}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should run fine', async () => {
|
||||
try {
|
||||
await reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
});
|
||||
} catch (err) {
|
||||
fail('Should run without error');
|
||||
}
|
||||
});
|
||||
});
|
||||
+133
-1
@@ -1364,11 +1364,119 @@ describe('Parse.File testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('default should allow common types', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
for (const type of ['plain', 'txt', 'png', 'jpg', 'gif', 'doc']) {
|
||||
const file = new Parse.File(`parse-server-logo.${type}`, { base64: 'ParseA==' });
|
||||
await file.save();
|
||||
}
|
||||
});
|
||||
|
||||
it('works with a period in the file name', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = ['file.png.html', 'file.txt.png.html', 'file.png.txt.html'];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('works to stop invalid filenames', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = [
|
||||
'!invalid.png',
|
||||
'.png',
|
||||
'.html',
|
||||
' .html',
|
||||
'.png.html',
|
||||
'~invalid.png',
|
||||
'-invalid.png',
|
||||
];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_FILE_NAME, `Filename contains invalid characters.`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows file without extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = ['filenamewithoutextension'];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeResolved();
|
||||
}
|
||||
});
|
||||
|
||||
it('works with array', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['jpg'],
|
||||
fileExtensions: ['jpg', 'wav'],
|
||||
},
|
||||
});
|
||||
await expectAsync(
|
||||
@@ -1387,6 +1495,30 @@ describe('Parse.File testing', () => {
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'image/jpg',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
})
|
||||
).toBeResolved();
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'audio/wav',
|
||||
base64: 'UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA',
|
||||
}),
|
||||
})
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('works with array without Content-Type', async () => {
|
||||
|
||||
+241
-13
@@ -49,7 +49,9 @@ describe('ParseGraphQLServer', () => {
|
||||
let parseGraphQLServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
parseServer = await global.reconfigureServer({});
|
||||
parseServer = await global.reconfigureServer({
|
||||
maxUploadSize: '1kb',
|
||||
});
|
||||
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
|
||||
graphQLPath: '/graphql',
|
||||
playgroundPath: '/playground',
|
||||
@@ -122,15 +124,16 @@ describe('ParseGraphQLServer', () => {
|
||||
info: new Object(),
|
||||
config: new Object(),
|
||||
auth: new Object(),
|
||||
get: () => {},
|
||||
};
|
||||
const res = {
|
||||
set: () => {},
|
||||
};
|
||||
|
||||
it("should return schema and context with req's info, config and auth", async () => {
|
||||
const options = await parseGraphQLServer._getGraphQLOptions();
|
||||
expect(options.multipart).toEqual({
|
||||
fileSize: 20971520,
|
||||
});
|
||||
expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema);
|
||||
const contextResponse = options.context({ req });
|
||||
const contextResponse = await options.context({ req, res });
|
||||
expect(contextResponse.info).toEqual(req.info);
|
||||
expect(contextResponse.config).toEqual(req.config);
|
||||
expect(contextResponse.auth).toEqual(req.auth);
|
||||
@@ -5275,7 +5278,6 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
it('should only count', async () => {
|
||||
await prepareData();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const where = {
|
||||
@@ -6833,7 +6835,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
describe('Files Mutations', () => {
|
||||
describe('Create', () => {
|
||||
it_only_node_version('<17')('should return File object', async () => {
|
||||
it('should return File object', async () => {
|
||||
const clientMutationId = uuidv4();
|
||||
|
||||
parseServer = await global.reconfigureServer({
|
||||
@@ -9299,7 +9301,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result6[0].node.name).toEqual('imACountry3');
|
||||
});
|
||||
|
||||
it_only_node_version('<17')('should support files', async () => {
|
||||
it('should support files', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
@@ -9341,7 +9343,6 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
|
||||
expect(result.data.createFile.fileInfo.name).toEqual(
|
||||
jasmine.stringMatching(/_myFileName.txt$/)
|
||||
);
|
||||
@@ -9547,9 +9548,233 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_only_node_version('<17')('should not upload if file is too large', async () => {
|
||||
parseGraphQLServer.parseServer.config.maxUploadSize = '1kb';
|
||||
it('should support files on required file', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', {
|
||||
someField: { type: 'File', required: true },
|
||||
});
|
||||
await resetGraphQLCache();
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation CreateSomeObject(
|
||||
$fields: CreateSomeClassWithRequiredFileFieldsInput
|
||||
) {
|
||||
createSomeClassWithRequiredFile(
|
||||
input: { fields: $fields }
|
||||
) {
|
||||
someClassWithRequiredFile {
|
||||
id
|
||||
someField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
fields: {
|
||||
someField: { upload: null },
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
body.append('map', JSON.stringify({ 1: ['variables.fields.someField.upload'] }));
|
||||
body.append('1', 'My File Content', {
|
||||
filename: 'myFileName.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
const resText = await res.text();
|
||||
const result = JSON.parse(resText);
|
||||
expect(
|
||||
result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.name
|
||||
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
|
||||
expect(
|
||||
result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.url
|
||||
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should support file upload for on fly creation through pointer and relation', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
const schema = new Parse.Schema('SomeClass');
|
||||
schema.addFile('someFileField');
|
||||
schema.addPointer('somePointerField', 'SomeClass');
|
||||
schema.addRelation('someRelationField', 'SomeClass');
|
||||
await schema.save();
|
||||
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation UploadFiles(
|
||||
$fields: CreateSomeClassFieldsInput
|
||||
) {
|
||||
createSomeClass(
|
||||
input: { fields: $fields }
|
||||
) {
|
||||
someClass {
|
||||
id
|
||||
someFileField {
|
||||
name
|
||||
url
|
||||
}
|
||||
somePointerField {
|
||||
id
|
||||
someFileField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
someRelationField {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
someFileField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
fields: {
|
||||
someFileField: { upload: null },
|
||||
somePointerField: {
|
||||
createAndLink: {
|
||||
someFileField: { upload: null },
|
||||
},
|
||||
},
|
||||
someRelationField: {
|
||||
createAndAdd: [
|
||||
{
|
||||
someFileField: { upload: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
body.append(
|
||||
'map',
|
||||
JSON.stringify({
|
||||
1: ['variables.fields.someFileField.upload'],
|
||||
2: ['variables.fields.somePointerField.createAndLink.someFileField.upload'],
|
||||
3: ['variables.fields.someRelationField.createAndAdd.0.someFileField.upload'],
|
||||
})
|
||||
);
|
||||
body.append('1', 'My File Content someFileField', {
|
||||
filename: 'someFileField.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
body.append('2', 'My File Content somePointerField', {
|
||||
filename: 'somePointerField.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
body.append('3', 'My File Content someRelationField', {
|
||||
filename: 'someRelationField.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
const result = await res.json();
|
||||
expect(result.data.createSomeClass.someClass.someFileField.name).toEqual(
|
||||
jasmine.stringMatching(/_someFileField.txt$/)
|
||||
);
|
||||
expect(result.data.createSomeClass.someClass.somePointerField.someFileField.name).toEqual(
|
||||
jasmine.stringMatching(/_somePointerField.txt$/)
|
||||
);
|
||||
expect(
|
||||
result.data.createSomeClass.someClass.someRelationField.edges[0].node.someFileField.name
|
||||
).toEqual(jasmine.stringMatching(/_someRelationField.txt$/));
|
||||
});
|
||||
|
||||
it('should support files and add extension from mimetype', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation CreateFile($input: CreateFileInput!) {
|
||||
createFile(input: $input) {
|
||||
fileInfo {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
upload: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
body.append('map', JSON.stringify({ 1: ['variables.input.upload'] }));
|
||||
body.append('1', 'My File Content', {
|
||||
// No extension, the system should add it from mimetype
|
||||
filename: 'myFileName',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
expect(result.data.createFile.fileInfo.name).toEqual(
|
||||
jasmine.stringMatching(/_myFileName.txt$/)
|
||||
);
|
||||
expect(result.data.createFile.fileInfo.url).toEqual(
|
||||
jasmine.stringMatching(/_myFileName.txt$/)
|
||||
);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not upload if file is too large', async () => {
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
@@ -9574,6 +9799,7 @@ describe('ParseGraphQLServer', () => {
|
||||
body.append('map', JSON.stringify({ 1: ['variables.input.upload'] }));
|
||||
body.append(
|
||||
'1',
|
||||
// In this test file parse server is setup with 1kb limit
|
||||
Buffer.alloc(parseGraphQLServer._transformMaxUploadSizeToBytes('2kb'), 1),
|
||||
{
|
||||
filename: 'myFileName.txt',
|
||||
@@ -9588,8 +9814,10 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
expect(res.status).toEqual(500);
|
||||
expect(result.errors[0].message).toEqual('File size limit exceeded: 1024 bytes');
|
||||
expect(res.status).toEqual(200);
|
||||
expect(result.errors[0].message).toEqual(
|
||||
'File truncated as it exceeds the 1024 byte size limit.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should support object values', async () => {
|
||||
|
||||
+34
-1
@@ -190,7 +190,7 @@ describe('Hooks', () => {
|
||||
|
||||
it('should fail trying to create two times the same function', done => {
|
||||
Parse.Hooks.createFunction('my_new_function', 'http://url.com')
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
.then(() => jasmine.timeout())
|
||||
.then(
|
||||
() => {
|
||||
return Parse.Hooks.createFunction('my_new_function', 'http://url.com');
|
||||
@@ -694,3 +694,36 @@ describe('triggers', () => {
|
||||
expect(req.context).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizing names', () => {
|
||||
const invalidNames = [
|
||||
`test'%3bdeclare%20@q%20varchar(99)%3bset%20@q%3d'%5c%5cxxxxxxxxxxxxxxx.yyyyy'%2b'fy.com%5cxus'%3b%20exec%20master.dbo.xp_dirtree%20@q%3b--%20`,
|
||||
`test.function.name`,
|
||||
];
|
||||
|
||||
it('should not crash server and return error on invalid Cloud Function name', async () => {
|
||||
for (const invalidName of invalidNames) {
|
||||
let error;
|
||||
try {
|
||||
await Parse.Cloud.run(invalidName);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toMatch(/Invalid function/);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not crash server and return error on invalid Cloud Job name', async () => {
|
||||
for (const invalidName of invalidNames) {
|
||||
let error;
|
||||
try {
|
||||
await Parse.Cloud.startJob(invalidName);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toMatch(/Invalid job/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+43
-81
@@ -4,6 +4,7 @@ const UserController = require('../lib/Controllers/UserController').UserControll
|
||||
const Config = require('../lib/Config');
|
||||
const ParseServer = require('../lib/index').ParseServer;
|
||||
const triggers = require('../lib/triggers');
|
||||
const { resolvingPromise, sleep } = require('./support/testUtils');
|
||||
const validatorFail = () => {
|
||||
throw 'you are not authorized';
|
||||
};
|
||||
@@ -14,19 +15,9 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
afterEach(async () => {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.close();
|
||||
// Wait for live query client to disconnect
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await client.close();
|
||||
});
|
||||
it('access user on onLiveQueryEvent disconnect', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const requestedUser = new Parse.User();
|
||||
requestedUser.setUsername('username');
|
||||
requestedUser.setPassword('password');
|
||||
@@ -43,18 +34,10 @@ describe('ParseLiveQuery', function () {
|
||||
const query = new Parse.Query(TestObject);
|
||||
await query.subscribe();
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.close();
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it('can subscribe to query', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -120,14 +103,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent payload', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -147,14 +122,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent enter', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('enter');
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -178,14 +145,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent leave', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('leave');
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -210,14 +169,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent delete', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('delete');
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -365,30 +316,18 @@ describe('ParseLiveQuery', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle afterEvent sendEvent to false', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
it('can handle afterEvent sendEvent to false', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const promise = resolvingPromise();
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
const current = req.object;
|
||||
const original = req.original;
|
||||
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, 2000);
|
||||
|
||||
if (current.get('foo') != original.get('foo')) {
|
||||
req.sendEvent = false;
|
||||
}
|
||||
promise.resolve();
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -402,9 +341,10 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
await promise;
|
||||
});
|
||||
|
||||
xit('can handle live query with fields - enable upon JS SDK support', async () => {
|
||||
it('can handle live query with fields', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['Test'],
|
||||
@@ -442,7 +382,7 @@ describe('ParseLiveQuery', function () {
|
||||
await obj2.save();
|
||||
obj2.set('foo', 'bart');
|
||||
await obj2.save();
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await sleep(2000);
|
||||
expect(createSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -771,14 +711,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('can handle select beforeSubscribe query', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.beforeSubscribe(TestObject, request => {
|
||||
const query = request.query;
|
||||
query.select('yolo');
|
||||
@@ -857,7 +789,7 @@ describe('ParseLiveQuery', function () {
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
await object.destroy();
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
await sleep(200);
|
||||
for (const key in calls) {
|
||||
expect(calls[key]).toHaveBeenCalled();
|
||||
}
|
||||
@@ -891,9 +823,9 @@ describe('ParseLiveQuery', function () {
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save(null, { useMasterKey: true });
|
||||
role.getUsers().add(user);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await sleep(1000);
|
||||
await role.save();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await sleep(1000);
|
||||
object.set('foo', 'yolo');
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
@@ -924,7 +856,6 @@ describe('ParseLiveQuery', function () {
|
||||
const subscription = await query.subscribe();
|
||||
|
||||
subscription.on('create', async obj => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
expect(obj.get('user').id).toBe(user.id);
|
||||
expect(obj.get('createdWith')).toEqual({ action: 'login', authProvider: 'password' });
|
||||
expect(obj.get('expiresAt')).toBeInstanceOf(Date);
|
||||
@@ -1227,6 +1158,8 @@ describe('ParseLiveQuery', function () {
|
||||
classNames: ['Yolo'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
};
|
||||
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
config.databaseAdapter = new databaseAdapter.constructor({
|
||||
@@ -1269,4 +1202,33 @@ describe('ParseLiveQuery', function () {
|
||||
expect(object2.id).toBeDefined();
|
||||
expect(object3.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('triggers query event with constraint not equal to null', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const spy = {
|
||||
create(obj) {
|
||||
expect(obj.attributes.foo).toEqual('bar');
|
||||
},
|
||||
};
|
||||
const createSpy = spyOn(spy, 'create');
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.notEqualTo('foo', null);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('create', spy.create);
|
||||
|
||||
const object1 = new TestObject();
|
||||
object1.set('foo', 'bar');
|
||||
await object1.save();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(createSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -356,7 +356,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
// Make sure we add subscriptionInfo to the client
|
||||
const args = client.addSubscriptionInfo.calls.first().args;
|
||||
expect(args[0]).toBe(requestId);
|
||||
expect(args[1].fields).toBe(query.fields);
|
||||
expect(args[1].keys).toBe(query.keys);
|
||||
expect(args[1].sessionToken).toBe(request.sessionToken);
|
||||
// Make sure we send subscribe response to the client
|
||||
expect(client.pushSubscribe).toHaveBeenCalledWith(requestId);
|
||||
@@ -417,7 +417,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
// Make sure we add subscriptionInfo to the client 2
|
||||
args = clientAgain.addSubscriptionInfo.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(requestIdAgain);
|
||||
expect(args[1].fields).toBe(queryAgain.fields);
|
||||
expect(args[1].keys).toBe(queryAgain.keys);
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command without clientId', function () {
|
||||
@@ -1081,7 +1081,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle create command with fields', async done => {
|
||||
it('can handle create command with keys', async done => {
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
|
||||
const Client = require('../lib/LiveQuery/Client').Client;
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
@@ -1131,61 +1131,6 @@ describe('ParseLiveQueryServer', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can deprecate fields', async () => {
|
||||
const Deprecator = require('../lib/Deprecator/Deprecator');
|
||||
const spy = spyOn(Deprecator, 'logRuntimeDeprecation').and.callFake(() => {});
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
|
||||
const Client = require('../lib/LiveQuery/Client').Client;
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
// Make mock request message
|
||||
const message = generateMockMessage();
|
||||
|
||||
const clientId = 1;
|
||||
const parseWebSocket = {
|
||||
clientId,
|
||||
send: jasmine.createSpy('send'),
|
||||
};
|
||||
const client = new Client(clientId, parseWebSocket);
|
||||
spyOn(client, 'pushCreate').and.callThrough();
|
||||
parseLiveQueryServer.clients.set(clientId, client);
|
||||
|
||||
// Add mock subscription
|
||||
const requestId = 2;
|
||||
const query = {
|
||||
className: testClassName,
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
fields: ['test'],
|
||||
};
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function (parseObject) {
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function () {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send create command to client
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).toHaveBeenCalled();
|
||||
const args = parseWebSocket.send.calls.mostRecent().args;
|
||||
const toSend = JSON.parse(args[0]);
|
||||
expect(toSend.object).toBeDefined();
|
||||
expect(toSend.original).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
usage: 'Subscribing using fields parameter',
|
||||
solution: `Subscribe using "keys" instead.`,
|
||||
});
|
||||
});
|
||||
|
||||
it('can handle create command with watch', async () => {
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
|
||||
const Client = require('../lib/LiveQuery/Client').Client;
|
||||
|
||||
@@ -568,6 +568,70 @@ describe('Parse.Object testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can increment array nested fields', async () => {
|
||||
const obj = new TestObject();
|
||||
obj.set('items', [ { value: 'a', count: 5 }, { value: 'b', count: 1 } ]);
|
||||
await obj.save();
|
||||
obj.increment('items.0.count', 15);
|
||||
obj.increment('items.1.count', 4);
|
||||
await obj.save();
|
||||
expect(obj.toJSON().items[0].value).toBe('a');
|
||||
expect(obj.toJSON().items[1].value).toBe('b');
|
||||
expect(obj.toJSON().items[0].count).toBe(20);
|
||||
expect(obj.toJSON().items[1].count).toBe(5);
|
||||
const query = new Parse.Query(TestObject);
|
||||
const result = await query.get(obj.id);
|
||||
expect(result.get('items')[0].value).toBe('a');
|
||||
expect(result.get('items')[1].value).toBe('b');
|
||||
expect(result.get('items')[0].count).toBe(20);
|
||||
expect(result.get('items')[1].count).toBe(5);
|
||||
expect(result.get('items')).toEqual(obj.get('items'));
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can increment array nested fields missing index', async () => {
|
||||
const obj = new TestObject();
|
||||
obj.set('items', []);
|
||||
await obj.save();
|
||||
obj.increment('items.1.count', 15);
|
||||
await obj.save();
|
||||
expect(obj.toJSON().items[0]).toBe(null);
|
||||
expect(obj.toJSON().items[1].count).toBe(15);
|
||||
const query = new Parse.Query(TestObject);
|
||||
const result = await query.get(obj.id);
|
||||
expect(result.get('items')[0]).toBe(null);
|
||||
expect(result.get('items')[1].count).toBe(15);
|
||||
expect(result.get('items')).toEqual(obj.get('items'));
|
||||
});
|
||||
|
||||
it('can query array nested fields', async () => {
|
||||
const objects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new TestObject();
|
||||
obj.set('items', [i, { value: i }]);
|
||||
objects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(objects);
|
||||
let query = new Parse.Query(TestObject);
|
||||
query.greaterThan('items.1.value', 5);
|
||||
let result = await query.find();
|
||||
expect(result.length).toBe(4);
|
||||
|
||||
query = new Parse.Query(TestObject);
|
||||
query.lessThan('items.0', 3);
|
||||
result = await query.find();
|
||||
expect(result.length).toBe(3);
|
||||
|
||||
query = new Parse.Query(TestObject);
|
||||
query.equalTo('items.0', 5);
|
||||
result = await query.find();
|
||||
expect(result.length).toBe(1);
|
||||
|
||||
query = new Parse.Query(TestObject);
|
||||
query.notEqualTo('items.0', 5);
|
||||
result = await query.find();
|
||||
expect(result.length).toBe(9);
|
||||
});
|
||||
|
||||
it('addUnique with object', function (done) {
|
||||
const x1 = new Parse.Object('X');
|
||||
x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]);
|
||||
@@ -2055,4 +2119,56 @@ describe('Parse.Object testing', () => {
|
||||
const object = new Parse.Object('CloudCodeIsNew');
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('should not change the json field to array in afterSave', async () => {
|
||||
Parse.Cloud.beforeSave('failingJSONTestCase', req => {
|
||||
expect(req.object.get('jsonField')).toEqual({ '123': 'test' });
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('failingJSONTestCase', req => {
|
||||
expect(req.object.get('jsonField')).toEqual({ '123': 'test' });
|
||||
});
|
||||
|
||||
const object = new Parse.Object('failingJSONTestCase');
|
||||
object.set('jsonField', { '123': 'test' });
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('returns correct field values', async () => {
|
||||
const values = [
|
||||
{ field: 'string', value: 'string' },
|
||||
{ field: 'number', value: 1 },
|
||||
{ field: 'boolean', value: true },
|
||||
{ field: 'array', value: [0, 1, 2] },
|
||||
{ field: 'array', value: [1, 2, 3] },
|
||||
{ field: 'array', value: [{ '0': 'a' }, 2, 3] },
|
||||
{ field: 'object', value: { key: 'value' } },
|
||||
{ field: 'object', value: { key1: 'value1', key2: 'value2' } },
|
||||
{ field: 'object', value: { key1: 1, key2: 2 } },
|
||||
{ field: 'object', value: { '1x1': 1 } },
|
||||
{ field: 'object', value: { '1x1': 1, '2': 2 } },
|
||||
{ field: 'object', value: { '0': 0 } },
|
||||
{ field: 'object', value: { '1': 1 } },
|
||||
{ field: 'object', value: { '0': { '0': 'a', '1': 'b' } } },
|
||||
{ field: 'date', value: new Date() },
|
||||
{
|
||||
field: 'file',
|
||||
value: Parse.File.fromJSON({
|
||||
__type: 'File',
|
||||
name: 'name',
|
||||
url: 'http://localhost:8378/1/files/test/name',
|
||||
}),
|
||||
},
|
||||
{ field: 'geoPoint', value: new Parse.GeoPoint(40, -30) },
|
||||
{ field: 'bytes', value: { __type: 'Bytes', base64: 'ZnJveW8=' } },
|
||||
];
|
||||
for (const value of values) {
|
||||
const object = new TestObject();
|
||||
object.set(value.field, value.value);
|
||||
await object.save();
|
||||
const query = new Parse.Query(TestObject);
|
||||
const objectAgain = await query.get(object.id);
|
||||
expect(objectAgain.get(value.field)).toEqual(value.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const Config = require('../lib/Config');
|
||||
const { MongoClient } = require('mongodb');
|
||||
const databaseURI = 'mongodb://localhost:27017/';
|
||||
const request = require('../lib/request');
|
||||
|
||||
let config, client, database;
|
||||
|
||||
const masterKeyHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const masterKeyOptions = {
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
};
|
||||
|
||||
const profileLevel = 2;
|
||||
describe_only_db('mongo')('Parse.Query with comment testing', () => {
|
||||
beforeAll(async () => {
|
||||
config = Config.get('test');
|
||||
client = await MongoClient.connect(databaseURI, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
});
|
||||
database = client.db('parseServerMongoAdapterTestDatabase');
|
||||
let profiler = await database.command({ profile: 0 });
|
||||
expect(profiler.was).toEqual(0);
|
||||
// console.log(`Disabling profiler : ${profiler.was}`);
|
||||
profiler = await database.command({ profile: profileLevel });
|
||||
profiler = await database.command({ profile: -1 });
|
||||
// console.log(`Enabling profiler : ${profiler.was}`);
|
||||
profiler = await database.command({ profile: -1 });
|
||||
expect(profiler.was).toEqual(profileLevel);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const profiler = await database.command({ profile: -1 });
|
||||
expect(profiler.was).toEqual(profileLevel);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await database.command({ profile: 0 });
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it('send comment with query through REST', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
comment: comment,
|
||||
},
|
||||
});
|
||||
await request(options);
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.explain.comment).toBe(comment);
|
||||
});
|
||||
|
||||
it('send comment with query', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
await collection._rawFind({ name: 'object' }, { comment: comment });
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.comment).toBe(comment);
|
||||
});
|
||||
|
||||
it('send a comment with a count query', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
|
||||
const object2 = new TestObject();
|
||||
object2.set('name', 'object');
|
||||
await object2.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
const countResult = await collection.count({ name: 'object' }, { comment: comment });
|
||||
expect(countResult).toEqual(2);
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.comment).toBe(comment);
|
||||
});
|
||||
|
||||
it('attach a comment to an aggregation', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
await collection.aggregate([{ $group: { _id: '$name' } }], {
|
||||
explain: true,
|
||||
comment: comment,
|
||||
});
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.explain.comment).toBe(comment);
|
||||
});
|
||||
});
|
||||
@@ -86,6 +86,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
|
||||
|
||||
@@ -93,6 +94,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
hint: '_id_',
|
||||
explain: true,
|
||||
});
|
||||
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('Parse Role testing', () => {
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
|
||||
let user, auth, getAllRolesSpy;
|
||||
createTestUser()
|
||||
|
||||
@@ -631,4 +631,58 @@ describe('ParseServerRESTController', () => {
|
||||
expect(sessions[0].get('installationId')).toBe(installationId);
|
||||
expect(sessions[0].get('sessionToken')).toBe(loggedUser.sessionToken);
|
||||
});
|
||||
|
||||
it('returns a statusId when running jobs', async () => {
|
||||
Parse.Cloud.job('CloudJob', () => {
|
||||
return 'Cloud job completed';
|
||||
});
|
||||
const res = await RESTController.request(
|
||||
'POST',
|
||||
'/jobs/CloudJob',
|
||||
{},
|
||||
{ useMasterKey: true, returnStatus: true }
|
||||
);
|
||||
const jobStatusId = res._headers['X-Parse-Job-Status-Id'];
|
||||
expect(jobStatusId).toBeDefined();
|
||||
const result = await Parse.Cloud.getJobStatus(jobStatusId);
|
||||
expect(result.id).toBe(jobStatusId);
|
||||
});
|
||||
|
||||
it('returns a statusId when running push notifications', async () => {
|
||||
const payload = {
|
||||
data: { alert: 'We return status!' },
|
||||
where: { deviceType: 'ios' },
|
||||
};
|
||||
const res = await RESTController.request('POST', '/push', payload, {
|
||||
useMasterKey: true,
|
||||
returnStatus: true,
|
||||
});
|
||||
const pushStatusId = res._headers['X-Parse-Push-Status-Id'];
|
||||
expect(pushStatusId).toBeDefined();
|
||||
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.id).toBe(pushStatusId);
|
||||
});
|
||||
|
||||
it('returns a statusId when running batch push notifications', async () => {
|
||||
const payload = {
|
||||
data: { alert: 'We return status!' },
|
||||
where: { deviceType: 'ios' },
|
||||
};
|
||||
const res = await RESTController.request('POST', 'batch', {
|
||||
requests: [{
|
||||
method: 'POST',
|
||||
path: '/push',
|
||||
body: payload,
|
||||
}],
|
||||
}, {
|
||||
useMasterKey: true,
|
||||
returnStatus: true,
|
||||
});
|
||||
const pushStatusId = res[0]._headers['X-Parse-Push-Status-Id'];
|
||||
expect(pushStatusId).toBeDefined();
|
||||
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.id).toBe(pushStatusId);
|
||||
});
|
||||
});
|
||||
|
||||
+92
-67
@@ -15,51 +15,18 @@ const cryptoUtils = require('../lib/cryptoUtils');
|
||||
|
||||
describe('allowExpiredAuthDataToken option', () => {
|
||||
it('should accept true value', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
await reconfigureServer({ allowExpiredAuthDataToken: true });
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(true);
|
||||
expect(
|
||||
logSpy.calls
|
||||
.all()
|
||||
.filter(
|
||||
log =>
|
||||
log.args[0] ===
|
||||
`DeprecationWarning: The Parse Server option 'allowExpiredAuthDataToken' default will change to 'false' in a future version.`
|
||||
).length
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('should accept false value', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
await reconfigureServer({ allowExpiredAuthDataToken: false });
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(false);
|
||||
expect(
|
||||
logSpy.calls
|
||||
.all()
|
||||
.filter(
|
||||
log =>
|
||||
log.args[0] ===
|
||||
`DeprecationWarning: The Parse Server option 'allowExpiredAuthDataToken' default will change to 'false' in a future version.`
|
||||
).length
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('should default true', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
it('should default false', async () => {
|
||||
await reconfigureServer({});
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(true);
|
||||
expect(
|
||||
logSpy.calls
|
||||
.all()
|
||||
.filter(
|
||||
log =>
|
||||
log.args[0] ===
|
||||
`DeprecationWarning: The Parse Server option 'allowExpiredAuthDataToken' default will change to 'false' in a future version.`
|
||||
).length
|
||||
).toEqual(1);
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(false);
|
||||
});
|
||||
|
||||
it('should enforce boolean values', async () => {
|
||||
@@ -107,6 +74,36 @@ describe('Parse.User testing', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('user login with context', async () => {
|
||||
let hit = 0;
|
||||
const context = { foo: 'bar' };
|
||||
Parse.Cloud.beforeLogin(req => {
|
||||
expect(req.context).toEqual(context);
|
||||
hit++;
|
||||
});
|
||||
Parse.Cloud.afterLogin(req => {
|
||||
expect(req.context).toEqual(context);
|
||||
hit++;
|
||||
});
|
||||
await Parse.User.signUp('asdf', 'zxcv');
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Cloud-Context': JSON.stringify(context),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
_method: 'GET',
|
||||
username: 'asdf',
|
||||
password: 'zxcv',
|
||||
},
|
||||
});
|
||||
expect(hit).toBe(2);
|
||||
});
|
||||
|
||||
it('user login with non-string username with REST API', async done => {
|
||||
await Parse.User.signUp('asdf', 'zxcv');
|
||||
request({
|
||||
@@ -1848,7 +1845,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow login with expired authData token by default', async () => {
|
||||
it('should not allow login with expired authData token since allowExpiredAuthDataToken is set to false by default', async () => {
|
||||
const provider = {
|
||||
authData: {
|
||||
id: '12345',
|
||||
@@ -1874,37 +1871,7 @@ describe('Parse.User testing', () => {
|
||||
// In this case, we want success as it was valid once.
|
||||
// If the client needs an updated token, do lock the user out
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken');
|
||||
await Parse.User._logInWith('shortLivedAuth', {});
|
||||
});
|
||||
|
||||
it('should not allow login with expired authData token when allowExpiredAuthDataToken is set to false', async () => {
|
||||
await reconfigureServer({ allowExpiredAuthDataToken: false });
|
||||
const provider = {
|
||||
authData: {
|
||||
id: '12345',
|
||||
access_token: 'token',
|
||||
},
|
||||
restoreAuthentication() {
|
||||
return true;
|
||||
},
|
||||
deauthenticate() {
|
||||
provider.authData = {};
|
||||
},
|
||||
authenticate(options) {
|
||||
options.success(this, provider.authData);
|
||||
},
|
||||
getAuthType() {
|
||||
return 'shortLivedAuth';
|
||||
},
|
||||
};
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('token');
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
await Parse.User._logInWith('shortLivedAuth', {});
|
||||
// Simulate a remotely expired token (like a short lived one)
|
||||
// In this case, we want success as it was valid once.
|
||||
// If the client needs an updated token, do lock the user out
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken');
|
||||
expectAsync(Parse.User._logInWith('shortLivedAuth', {})).toBeRejected();
|
||||
await expectAsync(Parse.User._logInWith('shortLivedAuth', {})).toBeRejected();
|
||||
});
|
||||
|
||||
it('should allow PUT request with stale auth Data', done => {
|
||||
@@ -3037,6 +3004,7 @@ describe('Parse.User testing', () => {
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
expect(emailCalled).toBe(true);
|
||||
expect(emailOptions).not.toBeUndefined();
|
||||
@@ -3194,6 +3162,35 @@ describe('Parse.User testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should return current session with expired expiration date', async () => {
|
||||
await Parse.User.signUp('buser', 'somepass', null);
|
||||
const response = await request({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/classes/_Session',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
const body = response.data;
|
||||
const id = body.results[0].objectId;
|
||||
const expiresAt = new Date(new Date().setYear(2015));
|
||||
await request({
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/classes/_Session/' + id,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
expiresAt: { __type: 'Date', iso: expiresAt.toISOString() },
|
||||
},
|
||||
});
|
||||
const session = await Parse.Session.current();
|
||||
expect(session.get('expiresAt')).toEqual(expiresAt);
|
||||
});
|
||||
|
||||
it('should not create extraneous session tokens', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
config.database
|
||||
@@ -4405,3 +4402,31 @@ describe('login as other user', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowClientClassCreation option', () => {
|
||||
it('should enforce boolean values', async () => {
|
||||
const options = [[], 'a', '', 0, 1, {}, 'true', 'false'];
|
||||
for (const option of options) {
|
||||
await expectAsync(reconfigureServer({ allowClientClassCreation: option })).toBeRejected();
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept true value', async () => {
|
||||
await reconfigureServer({ allowClientClassCreation: true });
|
||||
expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept false value', async () => {
|
||||
await reconfigureServer({ allowClientClassCreation: false });
|
||||
expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(false);
|
||||
});
|
||||
|
||||
it('should default false', async () => {
|
||||
// remove predefined allowClientClassCreation:true on global defaultConfiguration
|
||||
delete defaultConfiguration.allowClientClassCreation;
|
||||
await reconfigureServer(defaultConfiguration);
|
||||
expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(false);
|
||||
// Need to set it back to true to avoid other test fails
|
||||
defaultConfiguration.allowClientClassCreation = true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,14 +26,12 @@ const successfulIOS = function (body, installations) {
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const pushCompleted = async pushId => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
query.equalTo('objectId', pushId);
|
||||
let result = await query.first({ useMasterKey: true });
|
||||
while (!(result && result.get('status') === 'succeeded')) {
|
||||
await sleep(100);
|
||||
await jasmine.timeout();
|
||||
result = await query.first({ useMasterKey: true });
|
||||
}
|
||||
};
|
||||
@@ -562,7 +560,7 @@ describe('PushController', () => {
|
||||
});
|
||||
const pushStatusId = await sendPush(payload, {}, config, auth);
|
||||
// it is enqueued so it can take time
|
||||
await sleep(1000);
|
||||
await jasmine.timeout(1000);
|
||||
Parse.serverURL = 'http://localhost:8378/1'; // GOOD url
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result).toBeDefined();
|
||||
@@ -801,7 +799,7 @@ describe('PushController', () => {
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
await pushController.sendPush(payload, {}, config, auth);
|
||||
await sleep(1000);
|
||||
await jasmine.timeout(1000);
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
@@ -856,7 +854,7 @@ describe('PushController', () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
await Parse.Object.saveAll(installations);
|
||||
await pushController.sendPush(payload, {}, config, auth);
|
||||
await sleep(1000);
|
||||
await jasmine.timeout(1000);
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
|
||||
@@ -167,4 +167,18 @@ describe_only(() => {
|
||||
.then(() => expect(getQueueCount(cache)).toEqual(0))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should start and connect cache adapter', async () => {
|
||||
const server = await reconfigureServer({
|
||||
cacheAdapter: {
|
||||
module: `${__dirname.replace('/spec', '')}/lib/Adapters/Cache/RedisCacheAdapter`,
|
||||
options: {
|
||||
url: 'redis://127.0.0.1:6379/1',
|
||||
},
|
||||
},
|
||||
});
|
||||
const symbol = Object.getOwnPropertySymbols(server.config.cacheController);
|
||||
const client = server.config.cacheController[symbol[0]].client;
|
||||
expect(client.isOpen).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
+24
-20
@@ -399,15 +399,16 @@ describe('RestQuery.each', () => {
|
||||
}
|
||||
const config = Config.get('test');
|
||||
await Parse.Object.saveAll(objects);
|
||||
const query = new RestQuery(
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Object',
|
||||
{ value: { $gt: 2 } },
|
||||
{ limit: 2 }
|
||||
);
|
||||
auth: auth.master(config),
|
||||
className: 'Object',
|
||||
restWhere: { value: { $gt: 2 } },
|
||||
restOptions: { limit: 2 },
|
||||
});
|
||||
const spy = spyOn(query, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const results = [];
|
||||
await query.each(result => {
|
||||
expect(result.value).toBeGreaterThan(2);
|
||||
@@ -438,34 +439,37 @@ describe('RestQuery.each', () => {
|
||||
* Two queries needed since objectId are sorted and we can't know which one
|
||||
* going to be the first and then skip by the $gt added by each
|
||||
*/
|
||||
const queryOne = new RestQuery(
|
||||
const queryOne = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object1.id,
|
||||
},
|
||||
},
|
||||
{ limit: 1 }
|
||||
);
|
||||
const queryTwo = new RestQuery(
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
|
||||
const queryTwo = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object2.id,
|
||||
},
|
||||
},
|
||||
{ limit: 1 }
|
||||
);
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const resultsOne = [];
|
||||
const resultsTwo = [];
|
||||
await queryOne.each(result => {
|
||||
|
||||
@@ -337,5 +337,33 @@ describe('Security Check', () => {
|
||||
expect(logSpy.calls.all()[0].args[0]).toContain(title);
|
||||
}
|
||||
});
|
||||
|
||||
it('does update featuresRouter', async () => {
|
||||
let response = await request({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
expect(response.data.features.settings.securityCheck).toBeTrue();
|
||||
await reconfigureServer({
|
||||
security: {
|
||||
enableCheck: false,
|
||||
},
|
||||
});
|
||||
response = await request({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
expect(response.data.features.settings.securityCheck).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+50
-29
@@ -1,55 +1,76 @@
|
||||
const UserController = require('../lib/Controllers/UserController').UserController;
|
||||
const emailAdapter = require('./support/MockEmailAdapter');
|
||||
const Config = require('../lib/Config');
|
||||
const Auth = require('../lib/Auth');
|
||||
|
||||
describe('UserController', () => {
|
||||
const user = {
|
||||
_email_verify_token: 'testToken',
|
||||
username: 'testUser',
|
||||
email: 'test@example.com',
|
||||
};
|
||||
|
||||
describe('sendVerificationEmail', () => {
|
||||
describe('parseFrameURL not provided', () => {
|
||||
it('uses publicServerURL', async done => {
|
||||
it('uses publicServerURL', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://www.example.com',
|
||||
customPages: {
|
||||
parseFrameURL: undefined,
|
||||
},
|
||||
});
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
expect(options.link).toEqual(
|
||||
'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser'
|
||||
);
|
||||
emailAdapter.sendVerificationEmail = () => Promise.resolve();
|
||||
done();
|
||||
};
|
||||
const userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
appName: 'test',
|
||||
});
|
||||
userController.sendVerificationEmail(user);
|
||||
|
||||
let emailOptions;
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
emailOptions = options;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const username = 'verificationUser';
|
||||
const user = new Parse.User();
|
||||
user.setUsername(username);
|
||||
user.setPassword('pass');
|
||||
user.setEmail('verification@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const config = Config.get('test');
|
||||
const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config));
|
||||
const rawUsername = rawUser[0].username;
|
||||
const rawToken = rawUser[0]._email_verify_token;
|
||||
expect(rawToken).toBeDefined();
|
||||
expect(rawUsername).toBe(username);
|
||||
expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}&username=${username}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFrameURL provided', () => {
|
||||
it('uses parseFrameURL and includes the destination in the link parameter', async done => {
|
||||
it('uses parseFrameURL and includes the destination in the link parameter', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://www.example.com',
|
||||
customPages: {
|
||||
parseFrameURL: 'http://someother.example.com/handle-parse-iframe',
|
||||
},
|
||||
});
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
expect(options.link).toEqual(
|
||||
'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser'
|
||||
);
|
||||
emailAdapter.sendVerificationEmail = () => Promise.resolve();
|
||||
done();
|
||||
};
|
||||
const userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
appName: 'test',
|
||||
});
|
||||
userController.sendVerificationEmail(user);
|
||||
|
||||
let emailOptions;
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
emailOptions = options;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const username = 'verificationUser';
|
||||
const user = new Parse.User();
|
||||
user.setUsername(username);
|
||||
user.setPassword('pass');
|
||||
user.setEmail('verification@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const config = Config.get('test');
|
||||
const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config));
|
||||
const rawUsername = rawUser[0].username;
|
||||
const rawToken = rawUser[0]._email_verify_token;
|
||||
expect(rawToken).toBeDefined();
|
||||
expect(rawUsername).toBe(username);
|
||||
expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}&username=${username}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('zxcv');
|
||||
user.setEmail('testIfEnabled@parse.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
user.fetch().then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
@@ -137,6 +138,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake(options => {
|
||||
expect(options.link).not.toBeNull();
|
||||
expect(options.link).not.toMatch(/token=undefined/);
|
||||
expect(options.link).not.toMatch(/username=undefined/);
|
||||
Promise.resolve();
|
||||
});
|
||||
const user = new Parse.User();
|
||||
@@ -183,6 +185,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('zxcv');
|
||||
user.set('email', 'testSendSimpleAdapter@parse.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(calls).toBe(1);
|
||||
user
|
||||
.fetch()
|
||||
@@ -242,6 +245,66 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('prevents user from signup and login if email is not verified and preventLoginWithUnverifiedEmail is set to function returning true', async () => {
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: async () => true,
|
||||
preventLoginWithUnverifiedEmail: async () => true,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setPassword('asdf');
|
||||
user.setUsername('zxcv');
|
||||
user.set('email', 'testInvalidConfig@parse.com');
|
||||
const signupRes = await user.signUp(null).catch(e => e);
|
||||
expect(signupRes.message).toEqual('User email is not verified.');
|
||||
|
||||
const loginRes = await Parse.User.logIn('zxcv', 'asdf').catch(e => e);
|
||||
expect(loginRes.message).toEqual('User email is not verified.');
|
||||
});
|
||||
|
||||
it('provides function arguments in verifyUserEmails on login', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.set('email', 'test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const verifyUserEmails = {
|
||||
method: async (params) => {
|
||||
expect(params.object).toBeInstanceOf(Parse.User);
|
||||
expect(params.ip).toBeDefined();
|
||||
expect(params.master).toBeDefined();
|
||||
expect(params.installationId).toBeDefined();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/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', 'pass').catch(e => e);
|
||||
expect(res.code).toBe(205);
|
||||
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
@@ -263,6 +326,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('user');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(sendEmailOptions).not.toBeUndefined();
|
||||
const response = await request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -574,6 +638,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('zxcv');
|
||||
user.set('email', 'user@parse.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(emailSent).toBe(true);
|
||||
done();
|
||||
});
|
||||
@@ -601,6 +666,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
expect(sendEmailOptions).not.toBeUndefined();
|
||||
request({
|
||||
|
||||
@@ -585,4 +585,83 @@ describe('Verify User Password', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('verify password of user with unverified email with master key and ignoreEmailVerification=true', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/',
|
||||
appName: 'emailVerify',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.setEmail('test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const { data: res } = await request({
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/verifyPassword',
|
||||
headers: {
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
ignoreEmailVerification: true,
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
expect(res.objectId).toBe(user.id);
|
||||
expect(Object.prototype.hasOwnProperty.call(res, 'sessionToken')).toEqual(false);
|
||||
expect(Object.prototype.hasOwnProperty.call(res, 'password')).toEqual(false);
|
||||
});
|
||||
|
||||
it('fails to verify password of user with unverified email with master key and ignoreEmailVerification=false', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/',
|
||||
appName: 'emailVerify',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.setEmail('test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const res = await request({
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/verifyPassword',
|
||||
headers: {
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
ignoreEmailVerification: false,
|
||||
},
|
||||
json: true,
|
||||
}).catch(e => e);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.text).toMatch(/User email is not verified/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"appName": "test",
|
||||
"appId": "test",
|
||||
"masterKey": "test",
|
||||
"logLevel": "error",
|
||||
"auth": {
|
||||
"facebook": {
|
||||
"appIds": "test"
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
-17
@@ -35,6 +35,7 @@ process.noDeprecation = true;
|
||||
const cache = require('../lib/cache').default;
|
||||
const defaults = require('../lib/defaults').default;
|
||||
const ParseServer = require('../lib/index').ParseServer;
|
||||
const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter;
|
||||
const path = require('path');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
|
||||
@@ -53,7 +54,10 @@ let databaseAdapter;
|
||||
let databaseURI;
|
||||
// need to bind for mocking mocha
|
||||
|
||||
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
if (process.env.PARSE_SERVER_DATABASE_ADAPTER) {
|
||||
databaseAdapter = JSON.parse(process.env.PARSE_SERVER_DATABASE_ADAPTER);
|
||||
databaseAdapter = loadAdapter(databaseAdapter);
|
||||
} else if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
databaseURI = process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI;
|
||||
databaseAdapter = new PostgresStorageAdapter({
|
||||
uri: databaseURI,
|
||||
@@ -103,11 +107,17 @@ const defaultConfiguration = {
|
||||
restAPIKey: 'rest',
|
||||
webhookKey: 'hook',
|
||||
masterKey: 'test',
|
||||
maintenanceKey: 'testing',
|
||||
readOnlyMasterKey: 'read-only-test',
|
||||
fileKey: 'test',
|
||||
directAccess: true,
|
||||
silent,
|
||||
verbose: !silent,
|
||||
logLevel,
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
enableForAnonymousUser: true,
|
||||
@@ -128,8 +138,20 @@ const defaultConfiguration = {
|
||||
},
|
||||
shortLivedAuth: mockShortLivedAuth(),
|
||||
},
|
||||
allowClientClassCreation: true,
|
||||
encodeParseObjectInCloudFunction: true,
|
||||
};
|
||||
|
||||
if (silent) {
|
||||
defaultConfiguration.logLevels = {
|
||||
cloudFunctionSuccess: 'silent',
|
||||
cloudFunctionError: 'silent',
|
||||
triggerAfter: 'silent',
|
||||
triggerBeforeError: 'silent',
|
||||
triggerBeforeSuccess: 'silent',
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
|
||||
defaultConfiguration.cacheAdapter = new RedisCacheAdapter();
|
||||
}
|
||||
@@ -146,15 +168,15 @@ const destroyAliveConnections = function () {
|
||||
}
|
||||
};
|
||||
// Set up a default API server for testing with default configuration.
|
||||
let server;
|
||||
|
||||
let parseServer;
|
||||
let didChangeConfiguration = false;
|
||||
|
||||
// Allows testing specific configurations of Parse Server
|
||||
const reconfigureServer = async (changedConfiguration = {}) => {
|
||||
if (server) {
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
server = undefined;
|
||||
if (parseServer) {
|
||||
destroyAliveConnections();
|
||||
await new Promise(resolve => parseServer.server.close(resolve));
|
||||
parseServer = undefined;
|
||||
return reconfigureServer(changedConfiguration);
|
||||
}
|
||||
didChangeConfiguration = Object.keys(changedConfiguration).length !== 0;
|
||||
@@ -163,14 +185,20 @@ const reconfigureServer = async (changedConfiguration = {}) => {
|
||||
port,
|
||||
});
|
||||
cache.clear();
|
||||
const parseServer = await ParseServer.startApp(newConfiguration);
|
||||
server = parseServer.server;
|
||||
parseServer = await ParseServer.startApp(newConfiguration);
|
||||
Parse.CoreManager.setRESTController(RESTController);
|
||||
parseServer.expressApp.use('/1', err => {
|
||||
console.error(err);
|
||||
fail('should not call next');
|
||||
});
|
||||
server.on('connection', connection => {
|
||||
parseServer.liveQueryServer?.server?.on('connection', connection => {
|
||||
const key = `${connection.remoteAddress}:${connection.remotePort}`;
|
||||
openConnections[key] = connection;
|
||||
connection.on('close', () => {
|
||||
delete openConnections[key];
|
||||
});
|
||||
});
|
||||
parseServer.server.on('connection', connection => {
|
||||
const key = `${connection.remoteAddress}:${connection.remotePort}`;
|
||||
openConnections[key] = connection;
|
||||
connection.on('close', () => {
|
||||
@@ -198,16 +226,12 @@ beforeAll(async () => {
|
||||
Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
const afterLogOut = async () => {
|
||||
if (Object.keys(openConnections).length > 0) {
|
||||
console.warn('There were open connections to the server left after the test finished');
|
||||
// Jasmine process uses one connection
|
||||
if (Object.keys(openConnections).length > 1) {
|
||||
console.warn(`There were ${Object.keys(openConnections).length} open connections to the server left after the test finished`);
|
||||
}
|
||||
destroyAliveConnections();
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
SchemaCache.clear();
|
||||
if (didChangeConfiguration) {
|
||||
@@ -427,6 +451,26 @@ global.it_exclude_dbs = excluded => {
|
||||
}
|
||||
};
|
||||
|
||||
let testExclusionList = [];
|
||||
try {
|
||||
// Fetch test exclusion list
|
||||
testExclusionList = require('./testExclusionList.json');
|
||||
console.log(`Using test exclusion list with ${testExclusionList.length} entries`);
|
||||
} catch (error) {
|
||||
if (error.code !== 'MODULE_NOT_FOUND') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable test if its UUID is found in testExclusionList
|
||||
global.it_id = (id, func) => {
|
||||
if (testExclusionList.includes(id)) {
|
||||
return xit;
|
||||
} else {
|
||||
return func || it;
|
||||
}
|
||||
};
|
||||
|
||||
global.it_only_db = db => {
|
||||
if (
|
||||
process.env.PARSE_SERVER_TEST_DB === db ||
|
||||
@@ -546,6 +590,16 @@ global.describe_only_db = db => {
|
||||
}
|
||||
};
|
||||
|
||||
global.fdescribe_only_db = db => {
|
||||
if (process.env.PARSE_SERVER_TEST_DB == db) {
|
||||
return fdescribe;
|
||||
} else if (!process.env.PARSE_SERVER_TEST_DB && db == 'mongo') {
|
||||
return fdescribe;
|
||||
} else {
|
||||
return xdescribe;
|
||||
}
|
||||
};
|
||||
|
||||
global.describe_only = validator => {
|
||||
if (validator()) {
|
||||
return describe;
|
||||
@@ -571,4 +625,4 @@ jasmine.restoreLibrary = function (library, name) {
|
||||
require(library)[name] = libraryCache[library][name];
|
||||
};
|
||||
|
||||
jasmine.timeout = t => new Promise(resolve => setTimeout(resolve, t));
|
||||
jasmine.timeout = (t = 100) => new Promise(resolve => setTimeout(resolve, t));
|
||||
|
||||
@@ -136,6 +136,117 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with maintenance key', () => {
|
||||
let req;
|
||||
|
||||
async function getObject(id) {
|
||||
const res = await request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/TestObject/${id}`,
|
||||
});
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Maintenance-Key': 'testing',
|
||||
},
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
};
|
||||
});
|
||||
|
||||
it('allows createdAt', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
req.body = { createdAt };
|
||||
|
||||
const res = await request(req);
|
||||
expect(res.data.createdAt).toEqual(createdAt.iso);
|
||||
});
|
||||
|
||||
it('allows createdAt and updatedAt', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
|
||||
req.body = { createdAt, updatedAt };
|
||||
|
||||
const res = await request(req);
|
||||
|
||||
const obj = await getObject(res.data.objectId);
|
||||
expect(obj.createdAt).toEqual(createdAt.iso);
|
||||
expect(obj.updatedAt).toEqual(updatedAt.iso);
|
||||
});
|
||||
|
||||
it('allows createdAt, updatedAt, and additional field', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
|
||||
req.body = { createdAt, updatedAt, testing: 123 };
|
||||
|
||||
const res = await request(req);
|
||||
|
||||
const obj = await getObject(res.data.objectId);
|
||||
expect(obj.createdAt).toEqual(createdAt.iso);
|
||||
expect(obj.updatedAt).toEqual(updatedAt.iso);
|
||||
expect(obj.testing).toEqual(123);
|
||||
});
|
||||
|
||||
it('cannot set updatedAt dated before createdAt', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' };
|
||||
req.body = { createdAt, updatedAt };
|
||||
|
||||
try {
|
||||
await request(req);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.data.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
it('cannot set updatedAt without createdAt', async () => {
|
||||
const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' };
|
||||
req.body = { updatedAt };
|
||||
|
||||
const res = await request(req);
|
||||
|
||||
const obj = await getObject(res.data.objectId);
|
||||
expect(obj.updatedAt).not.toEqual(updatedAt.iso);
|
||||
});
|
||||
|
||||
it('handles bad types for createdAt and updatedAt', async () => {
|
||||
const createdAt = 12345;
|
||||
const updatedAt = true;
|
||||
req.body = { createdAt, updatedAt };
|
||||
|
||||
try {
|
||||
await request(req);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.data.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||
}
|
||||
});
|
||||
|
||||
it('cannot set createdAt or updatedAt without maintenance key', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
|
||||
req.body = { createdAt, updatedAt };
|
||||
delete req.headers['X-Parse-Maintenance-Key'];
|
||||
|
||||
const res = await request(req);
|
||||
|
||||
expect(res.data.createdAt).not.toEqual(createdAt.iso);
|
||||
expect(res.data.updatedAt).not.toEqual(updatedAt.iso);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles array, object, date', done => {
|
||||
const now = new Date();
|
||||
const obj = {
|
||||
@@ -660,6 +771,38 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get object in volatileClasses if not masterKey through pointer', async () => {
|
||||
const masterKeyOnlyClassObject = new Parse.Object('_PushStatus');
|
||||
await masterKeyOnlyClassObject.save(null, { useMasterKey: true });
|
||||
const obj2 = new Parse.Object('TestObject');
|
||||
// Anyone is can basically create a pointer to any object
|
||||
// or some developers can use master key in some hook to link
|
||||
// private objects to standard objects
|
||||
obj2.set('pointer', masterKeyOnlyClassObject);
|
||||
await obj2.save();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.include('pointer');
|
||||
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
|
||||
"Clients aren't allowed to perform the get operation on the _PushStatus collection."
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot get object in _GlobalConfig if not masterKey through pointer', async () => {
|
||||
await Parse.Config.save({ privateData: 'secret' }, { privateData: true });
|
||||
const obj2 = new Parse.Object('TestObject');
|
||||
obj2.set('globalConfigPointer', {
|
||||
__type: 'Pointer',
|
||||
className: '_GlobalConfig',
|
||||
objectId: 1,
|
||||
});
|
||||
await obj2.save();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.include('globalConfigPointer');
|
||||
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
|
||||
"Clients aren't allowed to perform the get operation on the _GlobalConfig collection."
|
||||
);
|
||||
});
|
||||
|
||||
it('locks down session', done => {
|
||||
let currentUser;
|
||||
Parse.User.signUp('foo', 'bar')
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = function (options) {
|
||||
return {
|
||||
options: options,
|
||||
send: function () {},
|
||||
getDatabaseURI: function () {
|
||||
return options.databaseURI;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -2,5 +2,5 @@
|
||||
"spec_dir": "spec",
|
||||
"spec_files": ["*spec.js"],
|
||||
"helpers": ["helper.js"],
|
||||
"random": true
|
||||
"random": false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Create Deferred Promise
|
||||
function resolvingPromise() {
|
||||
let res;
|
||||
let rej;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
res = resolve;
|
||||
rej = reject;
|
||||
});
|
||||
promise.resolve = res;
|
||||
promise.reject = rej;
|
||||
return promise;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
module.exports = { resolvingPromise, sleep };
|
||||
@@ -138,6 +138,71 @@ describe('Vulnerabilities', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('denies creating global config with polluted data', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
};
|
||||
const params = {
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/config',
|
||||
json: true,
|
||||
body: {
|
||||
params: {
|
||||
welcomeMesssage: 'Welcome to Parse',
|
||||
foo: { _bsontype: 'Code', code: 'shell' },
|
||||
},
|
||||
},
|
||||
headers,
|
||||
};
|
||||
const response = await request(params).catch(e => e);
|
||||
expect(response.status).toBe(400);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
|
||||
expect(text.error).toBe(
|
||||
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
|
||||
);
|
||||
});
|
||||
|
||||
it('denies direct database write wih prohibited keys', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const user = {
|
||||
objectId: '1234567890',
|
||||
username: 'hello',
|
||||
password: 'pass',
|
||||
_session_token: 'abc',
|
||||
foo: { _bsontype: 'Code', code: 'shell' },
|
||||
};
|
||||
await expectAsync(config.database.create('_User', user)).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies direct database update wih prohibited keys', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const user = {
|
||||
objectId: '1234567890',
|
||||
username: 'hello',
|
||||
password: 'pass',
|
||||
_session_token: 'abc',
|
||||
foo: { _bsontype: 'Code', code: 'shell' },
|
||||
};
|
||||
await expectAsync(
|
||||
config.database.update('_User', { _id: user.objectId }, user)
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies creating a hook with polluted data', async () => {
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
@@ -170,32 +235,6 @@ describe('Vulnerabilities', () => {
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('allows BSON type code data in write request with custom denylist', async () => {
|
||||
await reconfigureServer({
|
||||
requestKeywordDenylist: [],
|
||||
});
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const params = {
|
||||
headers: headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/RCE',
|
||||
body: JSON.stringify({
|
||||
obj: {
|
||||
_bsontype: 'Code',
|
||||
code: 'delete Object.prototype.evalFunctions',
|
||||
},
|
||||
}),
|
||||
};
|
||||
const response = await request(params).catch(e => e);
|
||||
expect(response.status).toBe(201);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.objectId).toBeDefined();
|
||||
});
|
||||
|
||||
it('denies write request with custom denylist of key/value', async () => {
|
||||
await reconfigureServer({
|
||||
requestKeywordDenylist: [{ key: 'a[K]ey', value: 'aValue[123]*' }],
|
||||
@@ -394,3 +433,28 @@ describe('Vulnerabilities', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Postgres regex sanitizater', () => {
|
||||
it('sanitizes the regex correctly to prevent Injection', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'username');
|
||||
user.set('password', 'password');
|
||||
user.set('email', 'email@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const response = await request({
|
||||
method: 'GET',
|
||||
url:
|
||||
"http://localhost:8378/1/classes/_User?where[username][$regex]=A'B'%3BSELECT+PG_SLEEP(3)%3B--",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.data.results).toEqual(jasmine.any(Array));
|
||||
expect(response.data.results.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,4 +46,21 @@ export function loadAdapter<T>(adapter, defaultAdapter, options): T {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
export async function loadModule(modulePath) {
|
||||
let module;
|
||||
try {
|
||||
module = require(modulePath);
|
||||
} catch (err) {
|
||||
if (err.code === 'ERR_REQUIRE_ESM') {
|
||||
module = await import(modulePath);
|
||||
if (module.default) {
|
||||
module = module.default;
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
export default loadAdapter;
|
||||
|
||||
@@ -21,7 +21,9 @@ export class AuthAdapter {
|
||||
* Usage policy
|
||||
* @type {AuthPolicy}
|
||||
*/
|
||||
this.policy = 'default';
|
||||
if (!this.policy) {
|
||||
this.policy = 'default';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param appIds The specified app IDs in the configuration
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
const jwksClient = require('jwks-rsa');
|
||||
const util = require('util');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('./utils');
|
||||
|
||||
@@ -17,11 +16,9 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
cacheMaxAge,
|
||||
});
|
||||
|
||||
const asyncGetSigningKeyFunction = util.promisify(client.getSigningKey);
|
||||
|
||||
let key;
|
||||
try {
|
||||
key = await asyncGetSigningKeyFunction(keyId);
|
||||
key = await authUtils.getSigningKey(client, keyId);
|
||||
} catch (error) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
const Parse = require('parse/node').Parse;
|
||||
const crypto = require('crypto');
|
||||
const jwksClient = require('jwks-rsa');
|
||||
const util = require('util');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const httpsRequest = require('./httpsRequest');
|
||||
const authUtils = require('./utils');
|
||||
|
||||
const TOKEN_ISSUER = 'https://facebook.com';
|
||||
const TOKEN_ISSUER = 'https://www.facebook.com';
|
||||
|
||||
function getAppSecretPath(authData, options = {}) {
|
||||
const appSecret = options.appSecret;
|
||||
@@ -60,11 +59,9 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
cacheMaxAge,
|
||||
});
|
||||
|
||||
const asyncGetSigningKeyFunction = util.promisify(client.getSigningKey);
|
||||
|
||||
let key;
|
||||
try {
|
||||
key = await asyncGetSigningKeyFunction(keyId);
|
||||
key = await authUtils.getSigningKey(client, keyId);
|
||||
} catch (error) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
|
||||
@@ -9,6 +9,7 @@ const facebook = require('./facebook');
|
||||
const instagram = require('./instagram');
|
||||
const linkedin = require('./linkedin');
|
||||
const meetup = require('./meetup');
|
||||
import mfa from './mfa';
|
||||
const google = require('./google');
|
||||
const github = require('./github');
|
||||
const twitter = require('./twitter');
|
||||
@@ -44,6 +45,7 @@ const providers = {
|
||||
instagram,
|
||||
linkedin,
|
||||
meetup,
|
||||
mfa,
|
||||
google,
|
||||
github,
|
||||
twitter,
|
||||
@@ -75,7 +77,11 @@ function authDataValidator(provider, adapter, appIds, options) {
|
||||
if (appIds && typeof adapter.validateAppId === 'function') {
|
||||
await Promise.resolve(adapter.validateAppId(appIds, authData, options, requestObject));
|
||||
}
|
||||
if (adapter.policy && !authAdapterPolicies[adapter.policy]) {
|
||||
if (
|
||||
adapter.policy &&
|
||||
!authAdapterPolicies[adapter.policy] &&
|
||||
typeof adapter.policy !== 'function'
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OTHER_CAUSE,
|
||||
'AuthAdapter policy is not configured correctly. The value must be either "solo", "additional", "default" or undefined (will be handled as "default")'
|
||||
@@ -225,17 +231,20 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
|
||||
if (!authAdapter) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
adapter: { afterFind },
|
||||
providerOptions,
|
||||
} = authAdapter;
|
||||
const { adapter, providerOptions } = authAdapter;
|
||||
const afterFind = adapter.afterFind;
|
||||
if (afterFind && typeof afterFind === 'function') {
|
||||
const requestObject = {
|
||||
ip: req.config.ip,
|
||||
user: req.auth.user,
|
||||
master: req.auth.isMaster,
|
||||
};
|
||||
const result = afterFind(requestObject, authData[provider], providerOptions);
|
||||
const result = afterFind.call(
|
||||
adapter,
|
||||
requestObject,
|
||||
authData[provider],
|
||||
providerOptions
|
||||
);
|
||||
if (result) {
|
||||
authData[provider] = result;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ function searchForGroup(client, options, id, resolve, reject) {
|
||||
return reject(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'LDAP group search failed'));
|
||||
}
|
||||
res.on('searchEntry', entry => {
|
||||
if (entry.object.cn === options.groupCn) {
|
||||
if (entry.pojo.attributes.find(obj => obj.type === 'cn').values.includes(options.groupCn)) {
|
||||
found = true;
|
||||
client.unbind();
|
||||
client.destroy();
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { TOTP, Secret } from 'otpauth';
|
||||
import { randomString } from '../../cryptoUtils';
|
||||
import AuthAdapter from './AuthAdapter';
|
||||
class MFAAdapter extends AuthAdapter {
|
||||
validateOptions(opts) {
|
||||
const validOptions = opts.options;
|
||||
if (!Array.isArray(validOptions)) {
|
||||
throw 'mfa.options must be an array';
|
||||
}
|
||||
this.sms = validOptions.includes('SMS');
|
||||
this.totp = validOptions.includes('TOTP');
|
||||
if (!this.sms && !this.totp) {
|
||||
throw 'mfa.options must include SMS or TOTP';
|
||||
}
|
||||
const digits = opts.digits || 6;
|
||||
const period = opts.period || 30;
|
||||
if (typeof digits !== 'number') {
|
||||
throw 'mfa.digits must be a number';
|
||||
}
|
||||
if (typeof period !== 'number') {
|
||||
throw 'mfa.period must be a number';
|
||||
}
|
||||
if (digits < 4 || digits > 10) {
|
||||
throw 'mfa.digits must be between 4 and 10';
|
||||
}
|
||||
if (period < 10) {
|
||||
throw 'mfa.period must be greater than 10';
|
||||
}
|
||||
const sendSMS = opts.sendSMS;
|
||||
if (this.sms && typeof sendSMS !== 'function') {
|
||||
throw 'mfa.sendSMS callback must be defined when using SMS OTPs';
|
||||
}
|
||||
this.smsCallback = sendSMS;
|
||||
this.digits = digits;
|
||||
this.period = period;
|
||||
this.algorithm = opts.algorithm || 'SHA1';
|
||||
}
|
||||
validateSetUp(mfaData) {
|
||||
if (mfaData.mobile && this.sms) {
|
||||
return this.setupMobileOTP(mfaData.mobile);
|
||||
}
|
||||
if (this.totp) {
|
||||
return this.setupTOTP(mfaData);
|
||||
}
|
||||
throw 'Invalid MFA data';
|
||||
}
|
||||
async validateLogin(loginData, _, req) {
|
||||
const saveResponse = {
|
||||
doNotSave: true,
|
||||
};
|
||||
const token = loginData.token;
|
||||
const auth = req.original.get('authData') || {};
|
||||
const { secret, recovery, mobile, token: saved, expiry } = auth.mfa || {};
|
||||
if (this.sms && mobile) {
|
||||
if (token === 'request') {
|
||||
const { token: sendToken, expiry } = await this.sendSMS(mobile);
|
||||
auth.mfa.token = sendToken;
|
||||
auth.mfa.expiry = expiry;
|
||||
req.object.set('authData', auth);
|
||||
await req.object.save(null, { useMasterKey: true });
|
||||
throw 'Please enter the token';
|
||||
}
|
||||
if (!saved || token !== saved) {
|
||||
throw 'Invalid MFA token 1';
|
||||
}
|
||||
if (new Date() > expiry) {
|
||||
throw 'Invalid MFA token 2';
|
||||
}
|
||||
delete auth.mfa.token;
|
||||
delete auth.mfa.expiry;
|
||||
return {
|
||||
save: auth.mfa,
|
||||
};
|
||||
}
|
||||
if (this.totp) {
|
||||
if (typeof token !== 'string') {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
if (!secret) {
|
||||
return saveResponse;
|
||||
}
|
||||
if (recovery[0] === token || recovery[1] === token) {
|
||||
return saveResponse;
|
||||
}
|
||||
const totp = new TOTP({
|
||||
algorithm: this.algorithm,
|
||||
digits: this.digits,
|
||||
period: this.period,
|
||||
secret: Secret.fromBase32(secret),
|
||||
});
|
||||
const valid = totp.validate({
|
||||
token,
|
||||
});
|
||||
if (valid === null) {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
}
|
||||
return saveResponse;
|
||||
}
|
||||
async validateUpdate(authData, _, req) {
|
||||
if (req.master) {
|
||||
return;
|
||||
}
|
||||
if (authData.mobile && this.sms) {
|
||||
if (!authData.token) {
|
||||
throw 'MFA is already set up on this account';
|
||||
}
|
||||
return this.confirmSMSOTP(authData, req.original.get('authData')?.mfa || {});
|
||||
}
|
||||
if (this.totp) {
|
||||
await this.validateLogin({ token: authData.old }, null, req);
|
||||
return this.validateSetUp(authData);
|
||||
}
|
||||
throw 'Invalid MFA data';
|
||||
}
|
||||
afterFind(req, authData) {
|
||||
if (req.master) {
|
||||
return;
|
||||
}
|
||||
if (this.totp && authData.secret) {
|
||||
return {
|
||||
status: 'enabled',
|
||||
};
|
||||
}
|
||||
if (this.sms && authData.mobile) {
|
||||
return {
|
||||
status: 'enabled',
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'disabled',
|
||||
};
|
||||
}
|
||||
|
||||
policy(req, auth) {
|
||||
if (this.sms && auth?.pending && Object.keys(auth).length === 1) {
|
||||
return 'default';
|
||||
}
|
||||
return 'additional';
|
||||
}
|
||||
|
||||
async setupMobileOTP(mobile) {
|
||||
const { token, expiry } = await this.sendSMS(mobile);
|
||||
return {
|
||||
save: {
|
||||
pending: {
|
||||
[mobile]: {
|
||||
token,
|
||||
expiry,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async sendSMS(mobile) {
|
||||
if (!/^[+]*[(]{0,1}[0-9]{1,3}[)]{0,1}[-\s\./0-9]*$/g.test(mobile)) {
|
||||
throw 'Invalid mobile number.';
|
||||
}
|
||||
let token = '';
|
||||
while (token.length < this.digits) {
|
||||
token += randomString(10).replace(/\D/g, '');
|
||||
}
|
||||
token = token.substring(0, this.digits);
|
||||
await Promise.resolve(this.smsCallback(token, mobile));
|
||||
const expiry = new Date(new Date().getTime() + this.period * 1000);
|
||||
return { token, expiry };
|
||||
}
|
||||
|
||||
async confirmSMSOTP(inputData, authData) {
|
||||
const { mobile, token } = inputData;
|
||||
if (!authData.pending?.[mobile]) {
|
||||
throw 'This number is not pending';
|
||||
}
|
||||
const pendingData = authData.pending[mobile];
|
||||
if (token !== pendingData.token) {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
if (new Date() > pendingData.expiry) {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
delete authData.pending[mobile];
|
||||
authData.mobile = mobile;
|
||||
return {
|
||||
save: authData,
|
||||
};
|
||||
}
|
||||
|
||||
setupTOTP(mfaData) {
|
||||
const { secret, token } = mfaData;
|
||||
if (!secret || !token || secret.length < 20) {
|
||||
throw 'Invalid MFA data';
|
||||
}
|
||||
const totp = new TOTP({
|
||||
algorithm: this.algorithm,
|
||||
digits: this.digits,
|
||||
period: this.period,
|
||||
secret: Secret.fromBase32(secret),
|
||||
});
|
||||
const valid = totp.validate({
|
||||
token,
|
||||
});
|
||||
if (valid === null) {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
const recovery = [randomString(30), randomString(30)];
|
||||
return {
|
||||
response: { recovery: recovery.join(', ') },
|
||||
save: { secret, recovery },
|
||||
};
|
||||
}
|
||||
}
|
||||
export default new MFAAdapter();
|
||||
@@ -1,4 +1,5 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const Parse = require('parse/node').Parse;
|
||||
const getHeaderFromToken = token => {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
@@ -8,6 +9,16 @@ const getHeaderFromToken = token => {
|
||||
|
||||
return decodedToken.header;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the signing key from a JWKS client.
|
||||
* @param {Object} client The JWKS client.
|
||||
* @param {String} key The kid.
|
||||
*/
|
||||
async function getSigningKey(client, key) {
|
||||
return util.promisify(client.getSigningKey)(key);
|
||||
}
|
||||
module.exports = {
|
||||
getHeaderFromToken,
|
||||
getSigningKey,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,10 @@ export class RedisCacheAdapter {
|
||||
this.ttl = isValidTTL(ttl) ? ttl : DEFAULT_REDIS_TTL;
|
||||
this.client = createClient(redisCtx);
|
||||
this.queue = new KeyPromiseQueue();
|
||||
this.client.on('error', err => { logger.error('RedisCacheAdapter client error', { error: err }) });
|
||||
this.client.on('connect', () => {});
|
||||
this.client.on('reconnecting', () => {});
|
||||
this.client.on('ready', () => {});
|
||||
}
|
||||
|
||||
async connect() {
|
||||
|
||||
@@ -28,7 +28,11 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
this._algorithm = 'aes-256-gcm';
|
||||
this._encryptionKey =
|
||||
encryptionKey !== undefined
|
||||
? crypto.createHash('sha256').update(String(encryptionKey)).digest('base64').substr(0, 32)
|
||||
? crypto
|
||||
.createHash('sha256')
|
||||
.update(String(encryptionKey))
|
||||
.digest('base64')
|
||||
.substring(0, 32)
|
||||
: null;
|
||||
const defaultMongoOptions = {
|
||||
useNewUrlParser: true,
|
||||
@@ -138,8 +142,8 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
}
|
||||
|
||||
async rotateEncryptionKey(options = {}) {
|
||||
var fileNames = [];
|
||||
var oldKeyFileAdapter = {};
|
||||
let fileNames = [];
|
||||
let oldKeyFileAdapter = {};
|
||||
const bucket = await this._getBucket();
|
||||
if (options.oldKey !== undefined) {
|
||||
oldKeyFileAdapter = new GridFSBucketAdapter(
|
||||
@@ -158,51 +162,22 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
fileNames.push(file.filename);
|
||||
});
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
var fileNamesNotRotated = fileNames;
|
||||
var fileNamesRotated = [];
|
||||
var fileNameTotal = fileNames.length;
|
||||
var fileNameIndex = 0;
|
||||
fileNames.forEach(fileName => {
|
||||
oldKeyFileAdapter
|
||||
.getFileData(fileName)
|
||||
.then(plainTextData => {
|
||||
//Overwrite file with data encrypted with new key
|
||||
this.createFile(fileName, plainTextData)
|
||||
.then(() => {
|
||||
fileNamesRotated.push(fileName);
|
||||
fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
|
||||
return value !== fileName;
|
||||
});
|
||||
fileNameIndex += 1;
|
||||
if (fileNameIndex == fileNameTotal) {
|
||||
resolve({
|
||||
rotated: fileNamesRotated,
|
||||
notRotated: fileNamesNotRotated,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
fileNameIndex += 1;
|
||||
if (fileNameIndex == fileNameTotal) {
|
||||
resolve({
|
||||
rotated: fileNamesRotated,
|
||||
notRotated: fileNamesNotRotated,
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
fileNameIndex += 1;
|
||||
if (fileNameIndex == fileNameTotal) {
|
||||
resolve({
|
||||
rotated: fileNamesRotated,
|
||||
notRotated: fileNamesNotRotated,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
let fileNamesNotRotated = fileNames;
|
||||
const fileNamesRotated = [];
|
||||
for (const fileName of fileNames) {
|
||||
try {
|
||||
const plainTextData = await oldKeyFileAdapter.getFileData(fileName);
|
||||
// Overwrite file with data encrypted with new key
|
||||
await this.createFile(fileName, plainTextData);
|
||||
fileNamesRotated.push(fileName);
|
||||
fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
|
||||
return value !== fileName;
|
||||
});
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return { rotated: fileNamesRotated, notRotated: fileNamesNotRotated };
|
||||
}
|
||||
|
||||
getFileLocation(config, filename) {
|
||||
|
||||
@@ -15,7 +15,18 @@ export default class MongoCollection {
|
||||
// idea. Or even if this behavior is a good idea.
|
||||
find(
|
||||
query,
|
||||
{ skip, limit, sort, keys, maxTimeMS, readPreference, hint, caseInsensitive, explain } = {}
|
||||
{
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
} = {}
|
||||
) {
|
||||
// Support for Full Text Search - $text
|
||||
if (keys && keys.$score) {
|
||||
@@ -32,6 +43,7 @@ export default class MongoCollection {
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
}).catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
|
||||
@@ -60,6 +72,7 @@ export default class MongoCollection {
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -75,7 +88,18 @@ export default class MongoCollection {
|
||||
|
||||
_rawFind(
|
||||
query,
|
||||
{ skip, limit, sort, keys, maxTimeMS, readPreference, hint, caseInsensitive, explain } = {}
|
||||
{
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
} = {}
|
||||
) {
|
||||
let findOperation = this._mongoCollection.find(query, {
|
||||
skip,
|
||||
@@ -83,6 +107,7 @@ export default class MongoCollection {
|
||||
sort,
|
||||
readPreference,
|
||||
hint,
|
||||
comment,
|
||||
});
|
||||
|
||||
if (keys) {
|
||||
@@ -100,7 +125,7 @@ export default class MongoCollection {
|
||||
return explain ? findOperation.explain(explain) : findOperation.toArray();
|
||||
}
|
||||
|
||||
count(query, { skip, limit, sort, maxTimeMS, readPreference, hint } = {}) {
|
||||
count(query, { skip, limit, sort, maxTimeMS, readPreference, hint, comment } = {}) {
|
||||
// If query is empty, then use estimatedDocumentCount instead.
|
||||
// This is due to countDocuments performing a scan,
|
||||
// which greatly increases execution time when being run on large collections.
|
||||
@@ -118,6 +143,7 @@ export default class MongoCollection {
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
comment,
|
||||
});
|
||||
|
||||
return countOperation;
|
||||
@@ -127,9 +153,9 @@ export default class MongoCollection {
|
||||
return this._mongoCollection.distinct(field, query);
|
||||
}
|
||||
|
||||
aggregate(pipeline, { maxTimeMS, readPreference, hint, explain } = {}) {
|
||||
aggregate(pipeline, { maxTimeMS, readPreference, hint, explain, comment } = {}) {
|
||||
return this._mongoCollection
|
||||
.aggregate(pipeline, { maxTimeMS, readPreference, hint, explain })
|
||||
.aggregate(pipeline, { maxTimeMS, readPreference, hint, explain, comment })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
@@ -160,18 +186,10 @@ export default class MongoCollection {
|
||||
}
|
||||
|
||||
_ensureSparseUniqueIndexInBackground(indexRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._mongoCollection.createIndex(
|
||||
indexRequest,
|
||||
{ unique: true, background: true, sparse: true },
|
||||
error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
return this._mongoCollection.createIndex(indexRequest, {
|
||||
unique: true,
|
||||
background: true,
|
||||
sparse: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,6 @@ 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)
|
||||
.then(client => {
|
||||
// Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client
|
||||
@@ -604,7 +603,17 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
{ skip, limit, sort, keys, readPreference, hint, caseInsensitive, explain }: QueryOptions
|
||||
{
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
readPreference,
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
}: QueryOptions
|
||||
): Promise<any> {
|
||||
validateExplainValue(explain);
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
@@ -647,6 +656,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
})
|
||||
)
|
||||
.then(objects => {
|
||||
@@ -687,13 +697,8 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
};
|
||||
|
||||
return this._adaptiveCollection(className)
|
||||
.then(
|
||||
collection =>
|
||||
new Promise((resolve, reject) =>
|
||||
collection._mongoCollection.createIndex(indexCreationRequest, indexOptions, error =>
|
||||
error ? reject(error) : resolve()
|
||||
)
|
||||
)
|
||||
.then(collection =>
|
||||
collection._mongoCollection.createIndex(indexCreationRequest, indexOptions)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
@@ -741,7 +746,9 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
readPreference: ?string,
|
||||
hint: ?mixed
|
||||
_estimate: ?boolean,
|
||||
hint: ?mixed,
|
||||
comment: ?string
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
@@ -751,6 +758,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
comment,
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
@@ -783,7 +791,8 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
pipeline: any,
|
||||
readPreference: ?string,
|
||||
hint: ?mixed,
|
||||
explain?: boolean
|
||||
explain?: boolean,
|
||||
comment: ?string
|
||||
) {
|
||||
validateExplainValue(explain);
|
||||
let isPointerField = false;
|
||||
@@ -817,6 +826,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
hint,
|
||||
explain,
|
||||
comment,
|
||||
})
|
||||
)
|
||||
.then(results => {
|
||||
|
||||
@@ -457,6 +457,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
|
||||
);
|
||||
}
|
||||
value = mapValues(restValue, transformInteriorValue);
|
||||
|
||||
return { key: restKey, value };
|
||||
};
|
||||
|
||||
@@ -986,6 +987,13 @@ function transformUpdateOperator({ __op, amount, objects }, flatten) {
|
||||
return { __op: '$inc', arg: amount };
|
||||
}
|
||||
|
||||
case 'SetOnInsert':
|
||||
if (flatten) {
|
||||
return amount;
|
||||
} else {
|
||||
return { __op: '$setOnInsert', arg: amount };
|
||||
}
|
||||
|
||||
case 'Add':
|
||||
case 'AddUnique':
|
||||
if (!(objects instanceof Array)) {
|
||||
|
||||
@@ -175,6 +175,8 @@ const toPostgresSchema = schema => {
|
||||
return schema;
|
||||
};
|
||||
|
||||
const isArrayIndex = (arrayIndex) => Array.from(arrayIndex).every(c => c >= '0' && c <= '9');
|
||||
|
||||
const handleDotFields = object => {
|
||||
Object.keys(object).forEach(fieldName => {
|
||||
if (fieldName.indexOf('.') > -1) {
|
||||
@@ -207,7 +209,11 @@ const transformDotFieldToComponents = fieldName => {
|
||||
if (index === 0) {
|
||||
return `"${cmpt}"`;
|
||||
}
|
||||
return `'${cmpt}'`;
|
||||
if (isArrayIndex(cmpt)) {
|
||||
return Number(cmpt);
|
||||
} else {
|
||||
return `'${cmpt}'`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -231,7 +237,7 @@ const transformAggregateField = fieldName => {
|
||||
if (fieldName === '$_updated_at') {
|
||||
return 'updatedAt';
|
||||
}
|
||||
return fieldName.substr(1);
|
||||
return fieldName.substring(1);
|
||||
};
|
||||
|
||||
const validateKeys = object => {
|
||||
@@ -1921,14 +1927,14 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
};
|
||||
}
|
||||
if (object[fieldName] && schema.fields[fieldName].type === 'Polygon') {
|
||||
let coords = object[fieldName];
|
||||
coords = coords.substr(2, coords.length - 4).split('),(');
|
||||
coords = coords.map(point => {
|
||||
let coords = new String(object[fieldName]);
|
||||
coords = coords.substring(2, coords.length - 2).split('),(');
|
||||
const updatedCoords = coords.map(point => {
|
||||
return [parseFloat(point.split(',')[1]), parseFloat(point.split(',')[0])];
|
||||
});
|
||||
object[fieldName] = {
|
||||
__type: 'Polygon',
|
||||
coordinates: coords,
|
||||
coordinates: updatedCoords,
|
||||
};
|
||||
}
|
||||
if (object[fieldName] && schema.fields[fieldName].type === 'File') {
|
||||
@@ -2614,16 +2620,16 @@ function isAnyValueRegexStartsWith(values) {
|
||||
});
|
||||
}
|
||||
|
||||
function createLiteralRegex(remaining) {
|
||||
function createLiteralRegex(remaining: string) {
|
||||
return remaining
|
||||
.split('')
|
||||
.map(c => {
|
||||
const regex = RegExp('[0-9 ]|\\p{L}', 'u'); // Support all unicode letter chars
|
||||
const regex = RegExp('[0-9 ]|\\p{L}', 'u'); // Support all Unicode letter chars
|
||||
if (c.match(regex) !== null) {
|
||||
// don't escape alphanumeric characters
|
||||
// Don't escape alphanumeric characters
|
||||
return c;
|
||||
}
|
||||
// escape everything else (single quotes with single quotes, everything else with a backslash)
|
||||
// Escape everything else (single quotes with single quotes, everything else with a backslash)
|
||||
return c === `'` ? `''` : `\\${c}`;
|
||||
})
|
||||
.join('');
|
||||
@@ -2633,31 +2639,35 @@ function literalizeRegexPart(s: string) {
|
||||
const matcher1 = /\\Q((?!\\E).*)\\E$/;
|
||||
const result1: any = s.match(matcher1);
|
||||
if (result1 && result1.length > 1 && result1.index > -1) {
|
||||
// process regex that has a beginning and an end specified for the literal text
|
||||
const prefix = s.substr(0, result1.index);
|
||||
// Process Regex that has a beginning and an end specified for the literal text
|
||||
const prefix = s.substring(0, result1.index);
|
||||
const remaining = result1[1];
|
||||
|
||||
return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
|
||||
}
|
||||
|
||||
// process regex that has a beginning specified for the literal text
|
||||
// Process Regex that has a beginning specified for the literal text
|
||||
const matcher2 = /\\Q((?!\\E).*)$/;
|
||||
const result2: any = s.match(matcher2);
|
||||
if (result2 && result2.length > 1 && result2.index > -1) {
|
||||
const prefix = s.substr(0, result2.index);
|
||||
const prefix = s.substring(0, result2.index);
|
||||
const remaining = result2[1];
|
||||
|
||||
return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
|
||||
}
|
||||
|
||||
// remove all instances of \Q and \E from the remaining text & escape single quotes
|
||||
// Remove problematic chars from remaining text
|
||||
return s
|
||||
// Remove all instances of \Q and \E
|
||||
.replace(/([^\\])(\\E)/, '$1')
|
||||
.replace(/([^\\])(\\Q)/, '$1')
|
||||
.replace(/^\\E/, '')
|
||||
.replace(/^\\Q/, '')
|
||||
.replace(/([^'])'/, `$1''`)
|
||||
.replace(/^'([^'])/, `''$1`);
|
||||
// Ensure even number of single quote sequences by adding an extra single quote if needed;
|
||||
// this ensures that every single quote is escaped
|
||||
.replace(/'+/g, match => {
|
||||
return match.length % 2 === 0 ? match : match + "'";
|
||||
});
|
||||
}
|
||||
|
||||
var GeoPointCoder = {
|
||||
|
||||
@@ -19,6 +19,7 @@ export type QueryOptions = {
|
||||
caseInsensitive?: boolean,
|
||||
action?: string,
|
||||
addsField?: boolean,
|
||||
comment?: string,
|
||||
};
|
||||
|
||||
export type UpdateQueryOptions = {
|
||||
@@ -97,7 +98,8 @@ export interface StorageAdapter {
|
||||
query: QueryType,
|
||||
readPreference?: string,
|
||||
estimate?: boolean,
|
||||
hint?: mixed
|
||||
hint?: mixed,
|
||||
comment?: string
|
||||
): Promise<number>;
|
||||
distinct(
|
||||
className: string,
|
||||
@@ -111,7 +113,8 @@ export interface StorageAdapter {
|
||||
pipeline: any,
|
||||
readPreference: ?string,
|
||||
hint: ?mixed,
|
||||
explain?: boolean
|
||||
explain?: boolean,
|
||||
comment?: string
|
||||
): Promise<any>;
|
||||
performInitialization(options: ?any): Promise<void>;
|
||||
watch(callback: () => void): void;
|
||||
|
||||
+72
-35
@@ -1,7 +1,6 @@
|
||||
const Parse = require('parse/node');
|
||||
import { isDeepStrictEqual } from 'util';
|
||||
import { getRequestObject, resolveError } from './triggers';
|
||||
import Deprecator from './Deprecator/Deprecator';
|
||||
import { logger } from './logger';
|
||||
import RestQuery from './RestQuery';
|
||||
import RestWrite from './RestWrite';
|
||||
@@ -68,6 +67,17 @@ function nobody(config) {
|
||||
return new Auth({ config, isMaster: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether session should be updated based on last update time & session length.
|
||||
*/
|
||||
function shouldUpdateSessionExpiry(config, session) {
|
||||
const resetAfter = config.sessionLength / 2;
|
||||
const lastUpdated = new Date(session?.updatedAt);
|
||||
const skipRange = new Date();
|
||||
skipRange.setTime(skipRange.getTime() - resetAfter * 1000);
|
||||
return lastUpdated <= skipRange;
|
||||
}
|
||||
|
||||
const throttle = {};
|
||||
const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
|
||||
if (!config?.extendSessionOnUse) {
|
||||
@@ -77,19 +87,19 @@ const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
|
||||
throttle[sessionToken] = setTimeout(async () => {
|
||||
try {
|
||||
if (!session) {
|
||||
const { results } = await new RestQuery(
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
master(config),
|
||||
'_Session',
|
||||
{ sessionToken },
|
||||
{ limit: 1 }
|
||||
).execute();
|
||||
auth: master(config),
|
||||
runBeforeFind: false,
|
||||
className: '_Session',
|
||||
restWhere: { sessionToken },
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
const { results } = await query.execute();
|
||||
session = results[0];
|
||||
}
|
||||
const lastUpdated = new Date(session?.updatedAt);
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
if (lastUpdated > yesterday || !session) {
|
||||
if (!shouldUpdateSessionExpiry(config, session) || !session) {
|
||||
return;
|
||||
}
|
||||
const expiresAt = config.generateSessionExpiresAt();
|
||||
@@ -140,7 +150,15 @@ const getAuthForSessionToken = async function ({
|
||||
include: 'user',
|
||||
};
|
||||
const RestQuery = require('./RestQuery');
|
||||
const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
runBeforeFind: false,
|
||||
auth: master(config),
|
||||
className: '_Session',
|
||||
restWhere: { sessionToken },
|
||||
restOptions,
|
||||
});
|
||||
results = (await query.execute()).results;
|
||||
} else {
|
||||
results = (
|
||||
@@ -179,12 +197,20 @@ const getAuthForSessionToken = async function ({
|
||||
});
|
||||
};
|
||||
|
||||
var getAuthForLegacySessionToken = function ({ config, sessionToken, installationId }) {
|
||||
var getAuthForLegacySessionToken = async function ({ config, sessionToken, installationId }) {
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
};
|
||||
const RestQuery = require('./RestQuery');
|
||||
var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions);
|
||||
var query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
runBeforeFind: false,
|
||||
auth: master(config),
|
||||
className: '_User',
|
||||
restWhere: { _session_token: sessionToken },
|
||||
restOptions,
|
||||
});
|
||||
return query.execute().then(response => {
|
||||
var results = response.results;
|
||||
if (results.length !== 1) {
|
||||
@@ -229,9 +255,15 @@ Auth.prototype.getRolesForUser = async function () {
|
||||
},
|
||||
};
|
||||
const RestQuery = require('./RestQuery');
|
||||
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
|
||||
results.push(result)
|
||||
);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
runBeforeFind: false,
|
||||
config: this.config,
|
||||
auth: master(this.config),
|
||||
className: '_Role',
|
||||
restWhere,
|
||||
});
|
||||
await query.each(result => results.push(result));
|
||||
} else {
|
||||
await new Parse.Query(Parse.Role)
|
||||
.equalTo('users', this.user)
|
||||
@@ -323,9 +355,15 @@ Auth.prototype.getRolesByIds = async function (ins) {
|
||||
});
|
||||
const restWhere = { roles: { $in: roles } };
|
||||
const RestQuery = require('./RestQuery');
|
||||
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
|
||||
results.push(result)
|
||||
);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
runBeforeFind: false,
|
||||
auth: master(this.config),
|
||||
className: '_Role',
|
||||
restWhere,
|
||||
});
|
||||
await query.each(result => results.push(result));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
@@ -407,6 +445,7 @@ const hasMutatedAuthData = (authData, userAuthData) => {
|
||||
};
|
||||
|
||||
const checkIfUserHasProvidedConfiguredProvidersForLogin = (
|
||||
req = {},
|
||||
authData = {},
|
||||
userAuthData = {},
|
||||
config
|
||||
@@ -430,7 +469,16 @@ const checkIfUserHasProvidedConfiguredProvidersForLogin = (
|
||||
|
||||
const additionProvidersNotFound = [];
|
||||
const hasProvidedAtLeastOneAdditionalProvider = savedUserProviders.some(provider => {
|
||||
if (provider && provider.adapter && provider.adapter.policy === 'additional') {
|
||||
let policy = provider.adapter.policy;
|
||||
if (typeof policy === 'function') {
|
||||
const requestObject = {
|
||||
ip: req.config.ip,
|
||||
user: req.auth.user,
|
||||
master: req.auth.isMaster,
|
||||
};
|
||||
policy = policy.call(provider.adapter, requestObject, userAuthData[provider.name]);
|
||||
}
|
||||
if (policy === 'additional') {
|
||||
if (authData[provider.name]) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -467,14 +515,8 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
|
||||
await user.fetch({ useMasterKey: true });
|
||||
}
|
||||
|
||||
const { originalObject, updatedObject } = req.buildParseObjects();
|
||||
const requestObject = getRequestObject(
|
||||
undefined,
|
||||
req.auth,
|
||||
updatedObject,
|
||||
originalObject || user,
|
||||
req.config
|
||||
);
|
||||
const { updatedObject } = req.buildParseObjects();
|
||||
const requestObject = getRequestObject(undefined, req.auth, updatedObject, user, req.config);
|
||||
// Perform validation as step-by-step pipeline for better error consistency
|
||||
// and also to avoid to trigger a provider (like OTP SMS) if another one fails
|
||||
const acc = { authData: {}, authDataResponse: {} };
|
||||
@@ -488,12 +530,6 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
|
||||
}
|
||||
const { validator } = req.config.authDataManager.getValidatorForProvider(provider);
|
||||
const authProvider = (req.config.auth || {})[provider] || {};
|
||||
if (authProvider.enabled == null) {
|
||||
Deprecator.logRuntimeDeprecation({
|
||||
usage: `Using the authentication adapter "${provider}" without explicitly enabling it`,
|
||||
solution: `Enable the authentication adapter by setting the Parse Server option "auth.${provider}.enabled: true".`,
|
||||
});
|
||||
}
|
||||
if (!validator || authProvider.enabled === false) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.UNSUPPORTED_SERVICE,
|
||||
@@ -551,6 +587,7 @@ module.exports = {
|
||||
maintenance,
|
||||
nobody,
|
||||
readOnly,
|
||||
shouldUpdateSessionExpiry,
|
||||
getAuthForSessionToken,
|
||||
getAuthForLegacySessionToken,
|
||||
findUsersWithAuthData,
|
||||
|
||||
+22
-1
@@ -7,6 +7,7 @@ import net from 'net';
|
||||
import AppCache from './cache';
|
||||
import DatabaseController from './Controllers/DatabaseController';
|
||||
import { logLevels as validLogLevels } from './Controllers/LoggerController';
|
||||
import { version } from '../package.json';
|
||||
import {
|
||||
AccountLockoutOptions,
|
||||
DatabaseOptions,
|
||||
@@ -25,7 +26,7 @@ function removeTrailingSlash(str) {
|
||||
return str;
|
||||
}
|
||||
if (str.endsWith('/')) {
|
||||
str = str.substr(0, str.length - 1);
|
||||
str = str.substring(0, str.length - 1);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
@@ -50,6 +51,7 @@ export class Config {
|
||||
config.generateEmailVerifyTokenExpiresAt = config.generateEmailVerifyTokenExpiresAt.bind(
|
||||
config
|
||||
);
|
||||
config.version = version;
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -62,6 +64,7 @@ export class Config {
|
||||
}
|
||||
|
||||
static validateOptions({
|
||||
customPages,
|
||||
publicServerURL,
|
||||
revokeSessionOnPasswordReset,
|
||||
expireInactiveSessions,
|
||||
@@ -88,6 +91,7 @@ export class Config {
|
||||
rateLimit,
|
||||
databaseOptions,
|
||||
extendSessionOnUse,
|
||||
allowClientClassCreation,
|
||||
}) {
|
||||
if (masterKey === readOnlyMasterKey) {
|
||||
throw new Error('masterKey and readOnlyMasterKey should be different');
|
||||
@@ -130,6 +134,16 @@ export class Config {
|
||||
this.validateRateLimit(rateLimit);
|
||||
this.validateLogLevels(logLevels);
|
||||
this.validateDatabaseOptions(databaseOptions);
|
||||
this.validateCustomPages(customPages);
|
||||
this.validateAllowClientClassCreation(allowClientClassCreation);
|
||||
}
|
||||
|
||||
static validateCustomPages(customPages) {
|
||||
if (!customPages) return;
|
||||
|
||||
if (Object.prototype.toString.call(customPages) !== '[object Object]') {
|
||||
throw Error('Parse Server option customPages must be an object.');
|
||||
}
|
||||
}
|
||||
|
||||
static validateControllers({
|
||||
@@ -172,6 +186,12 @@ export class Config {
|
||||
}
|
||||
}
|
||||
|
||||
static validateAllowClientClassCreation(allowClientClassCreation) {
|
||||
if (typeof allowClientClassCreation !== 'boolean') {
|
||||
throw 'Parse Server option allowClientClassCreation must be a boolean.';
|
||||
}
|
||||
}
|
||||
|
||||
static validateSecurityOptions(security) {
|
||||
if (Object.prototype.toString.call(security) !== '[object Object]') {
|
||||
throw 'Parse Server option security must be an object.';
|
||||
@@ -559,6 +579,7 @@ export class Config {
|
||||
if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') {
|
||||
throw `databaseOptions must be an object`;
|
||||
}
|
||||
|
||||
if (databaseOptions.enableSchemaHooks === undefined) {
|
||||
databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default;
|
||||
} else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') {
|
||||
|
||||
@@ -279,6 +279,9 @@ const flattenUpdateOperatorsForCreate = object => {
|
||||
}
|
||||
object[key] = object[key].amount;
|
||||
break;
|
||||
case 'SetOnInsert':
|
||||
object[key] = object[key].amount;
|
||||
break;
|
||||
case 'Add':
|
||||
if (!(object[key].objects instanceof Array)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
|
||||
@@ -365,6 +368,22 @@ const relationSchema = {
|
||||
fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } },
|
||||
};
|
||||
|
||||
const convertEmailToLowercase = (object, className, options) => {
|
||||
if (className === '_User' && options.convertEmailToLowercase) {
|
||||
if (typeof object['email'] === 'string') {
|
||||
object['email'] = object['email'].toLowerCase();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const convertUsernameToLowercase = (object, className, options) => {
|
||||
if (className === '_User' && options.convertUsernameToLowercase) {
|
||||
if (typeof object['username'] === 'string') {
|
||||
object['username'] = object['username'].toLowerCase();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class DatabaseController {
|
||||
adapter: StorageAdapter;
|
||||
schemaCache: any;
|
||||
@@ -475,6 +494,11 @@ class DatabaseController {
|
||||
validateOnly: boolean = false,
|
||||
validSchemaController: SchemaController.SchemaController
|
||||
): Promise<any> {
|
||||
try {
|
||||
Utils.checkProhibitedKeywords(this.options, update);
|
||||
} catch (error) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
|
||||
}
|
||||
const originalQuery = query;
|
||||
const originalUpdate = update;
|
||||
// Make a copy of the object, so we don't mutate the incoming data.
|
||||
@@ -565,6 +589,8 @@ class DatabaseController {
|
||||
}
|
||||
}
|
||||
update = transformObjectACL(update);
|
||||
convertEmailToLowercase(update, className, this.options);
|
||||
convertUsernameToLowercase(update, className, this.options);
|
||||
transformAuthData(className, update, schema);
|
||||
if (validateOnly) {
|
||||
return this.adapter.find(className, schema, query, {}).then(result => {
|
||||
@@ -805,10 +831,17 @@ class DatabaseController {
|
||||
validateOnly: boolean = false,
|
||||
validSchemaController: SchemaController.SchemaController
|
||||
): Promise<any> {
|
||||
try {
|
||||
Utils.checkProhibitedKeywords(this.options, object);
|
||||
} catch (error) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
|
||||
}
|
||||
// Make a copy of the object, so we don't mutate the incoming data.
|
||||
const originalObject = object;
|
||||
object = transformObjectACL(object);
|
||||
|
||||
convertEmailToLowercase(object, className, this.options);
|
||||
convertUsernameToLowercase(object, className, this.options);
|
||||
object.createdAt = { iso: object.createdAt, __type: 'Date' };
|
||||
object.updatedAt = { iso: object.updatedAt, __type: 'Date' };
|
||||
|
||||
@@ -1155,6 +1188,7 @@ class DatabaseController {
|
||||
hint,
|
||||
caseInsensitive = false,
|
||||
explain,
|
||||
comment,
|
||||
}: any = {},
|
||||
auth: any = {},
|
||||
validSchemaController: SchemaController.SchemaController
|
||||
@@ -1202,8 +1236,9 @@ class DatabaseController {
|
||||
keys,
|
||||
readPreference,
|
||||
hint,
|
||||
caseInsensitive,
|
||||
caseInsensitive: this.options.enableCollationCaseComparison ? false : caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
};
|
||||
Object.keys(sort).forEach(fieldName => {
|
||||
if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
||||
@@ -1273,7 +1308,8 @@ class DatabaseController {
|
||||
query,
|
||||
readPreference,
|
||||
undefined,
|
||||
hint
|
||||
hint,
|
||||
comment
|
||||
);
|
||||
}
|
||||
} else if (distinct) {
|
||||
@@ -1292,7 +1328,8 @@ class DatabaseController {
|
||||
pipeline,
|
||||
readPreference,
|
||||
hint,
|
||||
explain
|
||||
explain,
|
||||
comment
|
||||
);
|
||||
}
|
||||
} else if (explain) {
|
||||
@@ -1706,31 +1743,27 @@ class DatabaseController {
|
||||
throw error;
|
||||
});
|
||||
|
||||
await this.adapter
|
||||
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
|
||||
.catch(error => {
|
||||
logger.warn('Unable to create case insensitive username index: ', error);
|
||||
throw error;
|
||||
});
|
||||
await this.adapter
|
||||
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
|
||||
.catch(error => {
|
||||
logger.warn('Unable to create case insensitive username index: ', error);
|
||||
throw error;
|
||||
});
|
||||
if (!this.options.enableCollationCaseComparison) {
|
||||
await this.adapter
|
||||
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
|
||||
.catch(error => {
|
||||
logger.warn('Unable to create case insensitive username index: ', error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
await this.adapter
|
||||
.ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
|
||||
.catch(error => {
|
||||
logger.warn('Unable to create case insensitive email index: ', error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => {
|
||||
logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
await this.adapter
|
||||
.ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
|
||||
.catch(error => {
|
||||
logger.warn('Unable to create case insensitive email index: ', error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => {
|
||||
logger.warn('Unable to ensure uniqueness for role name: ', error);
|
||||
throw error;
|
||||
@@ -1813,11 +1846,19 @@ class DatabaseController {
|
||||
keyUpdate &&
|
||||
typeof keyUpdate === 'object' &&
|
||||
keyUpdate.__op &&
|
||||
['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1
|
||||
['Add', 'AddUnique', 'Remove', 'Increment', 'SetOnInsert'].indexOf(keyUpdate.__op) > -1
|
||||
) {
|
||||
// only valid ops that produce an actionable result
|
||||
// the op may have happened on a keypath
|
||||
this._expandResultOnKeyPath(response, key, result);
|
||||
// Revert array to object conversion on dot notation for arrays (e.g. "field.0.key")
|
||||
if (key.includes('.')) {
|
||||
const [field, index] = key.split('.');
|
||||
const isArrayIndex = Array.from(index).every(c => c >= '0' && c <= '9');
|
||||
if (isArrayIndex && Array.isArray(result[field]) && !Array.isArray(response[field])) {
|
||||
response[field] = result[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return Promise.resolve(response);
|
||||
|
||||
@@ -16,7 +16,7 @@ export const LogOrder = {
|
||||
ASCENDING: 'asc',
|
||||
};
|
||||
|
||||
export const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly'];
|
||||
export const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly', 'silent'];
|
||||
|
||||
export class LoggerController extends AdaptableController {
|
||||
constructor(adapter, appId, options = { logLevel: 'info' }) {
|
||||
|
||||
@@ -58,9 +58,16 @@ export class PushController {
|
||||
|
||||
// Force filtering on only valid device tokens
|
||||
const updateWhere = applyDeviceTokenExists(where);
|
||||
badgeUpdate = () => {
|
||||
badgeUpdate = async () => {
|
||||
// Build a real RestQuery so we can use it in RestWrite
|
||||
const restQuery = new RestQuery(config, master(config), '_Installation', updateWhere);
|
||||
const restQuery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
runBeforeFind: false,
|
||||
auth: master(config),
|
||||
className: '_Installation',
|
||||
restWhere: updateWhere,
|
||||
});
|
||||
return restQuery.buildRestWhere().then(() => {
|
||||
const write = new RestWrite(
|
||||
config,
|
||||
|
||||
@@ -1096,9 +1096,17 @@ export default class SchemaController {
|
||||
maintenance?: boolean
|
||||
) {
|
||||
if (fieldName.indexOf('.') > 0) {
|
||||
// subdocument key (x.y) => ok if x is of type 'object'
|
||||
fieldName = fieldName.split('.')[0];
|
||||
type = 'Object';
|
||||
// "<array>.<index>" for Nested Arrays
|
||||
// "<embedded document>.<field>" for Nested Objects
|
||||
// JSON Arrays are treated as Nested Objects
|
||||
const [x, y] = fieldName.split('.');
|
||||
fieldName = x;
|
||||
const isArrayIndex = Array.from(y).every(c => c >= '0' && c <= '9');
|
||||
if (isArrayIndex && !['sentPerUTCOffset', 'failedPerUTCOffset'].includes(fieldName)) {
|
||||
type = 'Array';
|
||||
} else {
|
||||
type = 'Object';
|
||||
}
|
||||
}
|
||||
let fieldNameToValidate = `${fieldName}`;
|
||||
if (maintenance && fieldNameToValidate.charAt(0) === '_') {
|
||||
|
||||
@@ -32,23 +32,35 @@ export class UserController extends AdaptableController {
|
||||
}
|
||||
|
||||
get shouldVerifyEmails() {
|
||||
return this.options.verifyUserEmails;
|
||||
return (this.config || this.options).verifyUserEmails;
|
||||
}
|
||||
|
||||
setEmailVerifyToken(user) {
|
||||
if (this.shouldVerifyEmails) {
|
||||
user._email_verify_token = randomString(25);
|
||||
user.emailVerified = false;
|
||||
|
||||
if (this.config.emailVerifyTokenValidityDuration) {
|
||||
user._email_verify_token_expires_at = Parse._encode(
|
||||
this.config.generateEmailVerifyTokenExpiresAt()
|
||||
);
|
||||
}
|
||||
async setEmailVerifyToken(user, req, storage = {}) {
|
||||
const shouldSendEmail =
|
||||
this.shouldVerifyEmails === true ||
|
||||
(typeof this.shouldVerifyEmails === 'function' &&
|
||||
(await Promise.resolve(this.shouldVerifyEmails(req))) === true);
|
||||
if (!shouldSendEmail) {
|
||||
return false;
|
||||
}
|
||||
storage.sendVerificationEmail = true;
|
||||
user._email_verify_token = randomString(25);
|
||||
if (
|
||||
!storage.fieldsChangedByTrigger ||
|
||||
!storage.fieldsChangedByTrigger.includes('emailVerified')
|
||||
) {
|
||||
user.emailVerified = false;
|
||||
}
|
||||
|
||||
if (this.config.emailVerifyTokenValidityDuration) {
|
||||
user._email_verify_token_expires_at = Parse._encode(
|
||||
this.config.generateEmailVerifyTokenExpiresAt()
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
verifyEmail(username, token) {
|
||||
async verifyEmail(username, token) {
|
||||
if (!this.shouldVerifyEmails) {
|
||||
// Trying to verify email when not enabled
|
||||
// TODO: Better error here.
|
||||
@@ -70,8 +82,14 @@ export class UserController extends AdaptableController {
|
||||
updateFields._email_verify_token_expires_at = { __op: 'Delete' };
|
||||
}
|
||||
const maintenanceAuth = Auth.maintenance(this.config);
|
||||
var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', {
|
||||
username,
|
||||
var findUserForEmailVerification = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config: this.config,
|
||||
auth: maintenanceAuth,
|
||||
className: '_User',
|
||||
restWhere: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
return findUserForEmailVerification.execute().then(result => {
|
||||
if (result.results.length && result.results[0].emailVerified) {
|
||||
@@ -110,10 +128,7 @@ export class UserController extends AdaptableController {
|
||||
});
|
||||
}
|
||||
|
||||
getUserIfNeeded(user) {
|
||||
if (user.username && user.email) {
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
async getUserIfNeeded(user) {
|
||||
var where = {};
|
||||
if (user.username) {
|
||||
where.username = user.username;
|
||||
@@ -122,36 +137,55 @@ export class UserController extends AdaptableController {
|
||||
where.email = user.email;
|
||||
}
|
||||
|
||||
var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
|
||||
return query.execute().then(function (result) {
|
||||
if (result.results.length != 1) {
|
||||
throw undefined;
|
||||
}
|
||||
return result.results[0];
|
||||
var query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config: this.config,
|
||||
runBeforeFind: false,
|
||||
auth: Auth.master(this.config),
|
||||
className: '_User',
|
||||
restWhere: where,
|
||||
});
|
||||
const result = await query.execute();
|
||||
if (result.results.length != 1) {
|
||||
throw undefined;
|
||||
}
|
||||
return result.results[0];
|
||||
}
|
||||
|
||||
sendVerificationEmail(user) {
|
||||
async sendVerificationEmail(user, req) {
|
||||
if (!this.shouldVerifyEmails) {
|
||||
return;
|
||||
}
|
||||
const token = encodeURIComponent(user._email_verify_token);
|
||||
// We may need to fetch the user in case of update email
|
||||
this.getUserIfNeeded(user).then(user => {
|
||||
const username = encodeURIComponent(user.username);
|
||||
// We may need to fetch the user in case of update email; only use the `fetchedUser`
|
||||
// from this point onwards; do not use the `user` as it may not contain all fields.
|
||||
const fetchedUser = await this.getUserIfNeeded(user);
|
||||
let shouldSendEmail = this.config.sendUserEmailVerification;
|
||||
if (typeof shouldSendEmail === 'function') {
|
||||
const response = await Promise.resolve(
|
||||
this.config.sendUserEmailVerification({
|
||||
user: Parse.Object.fromJSON({ className: '_User', ...fetchedUser }),
|
||||
master: req.auth?.isMaster,
|
||||
})
|
||||
);
|
||||
shouldSendEmail = !!response;
|
||||
}
|
||||
if (!shouldSendEmail) {
|
||||
return;
|
||||
}
|
||||
const username = encodeURIComponent(fetchedUser.username);
|
||||
|
||||
const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
|
||||
const options = {
|
||||
appName: this.config.appName,
|
||||
link: link,
|
||||
user: inflate('_User', user),
|
||||
};
|
||||
if (this.adapter.sendVerificationEmail) {
|
||||
this.adapter.sendVerificationEmail(options);
|
||||
} else {
|
||||
this.adapter.sendMail(this.defaultVerificationEmail(options));
|
||||
}
|
||||
});
|
||||
const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
|
||||
const options = {
|
||||
appName: this.config.appName,
|
||||
link: link,
|
||||
user: inflate('_User', fetchedUser),
|
||||
};
|
||||
if (this.adapter.sendVerificationEmail) {
|
||||
this.adapter.sendVerificationEmail(options);
|
||||
} else {
|
||||
this.adapter.sendMail(this.defaultVerificationEmail(options));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +194,7 @@ export class UserController extends AdaptableController {
|
||||
* @param user
|
||||
* @returns {*}
|
||||
*/
|
||||
regenerateEmailVerifyToken(user) {
|
||||
async regenerateEmailVerifyToken(user, master, installationId, ip) {
|
||||
const { _email_verify_token } = user;
|
||||
let { _email_verify_token_expires_at } = user;
|
||||
if (_email_verify_token_expires_at && _email_verify_token_expires_at.__type === 'Date') {
|
||||
@@ -172,21 +206,30 @@ export class UserController extends AdaptableController {
|
||||
_email_verify_token &&
|
||||
new Date() < new Date(_email_verify_token_expires_at)
|
||||
) {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
const shouldSend = await this.setEmailVerifyToken(user, {
|
||||
object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)),
|
||||
master,
|
||||
installationId,
|
||||
ip,
|
||||
resendRequest: true
|
||||
});
|
||||
if (!shouldSend) {
|
||||
return;
|
||||
}
|
||||
this.setEmailVerifyToken(user);
|
||||
return this.config.database.update('_User', { username: user.username }, user);
|
||||
}
|
||||
|
||||
resendVerificationEmail(username) {
|
||||
return this.getUserIfNeeded({ username: username }).then(aUser => {
|
||||
if (!aUser || aUser.emailVerified) {
|
||||
throw undefined;
|
||||
}
|
||||
return this.regenerateEmailVerifyToken(aUser).then(() => {
|
||||
this.sendVerificationEmail(aUser);
|
||||
});
|
||||
});
|
||||
async resendVerificationEmail(username, req) {
|
||||
const aUser = await this.getUserIfNeeded({ username: username });
|
||||
if (!aUser || aUser.emailVerified) {
|
||||
throw undefined;
|
||||
}
|
||||
const generate = await this.regenerateEmailVerifyToken(aUser, req.auth?.isMaster, req.auth?.installationId, req.ip);
|
||||
if (generate) {
|
||||
this.sendVerificationEmail(aUser, req);
|
||||
}
|
||||
}
|
||||
|
||||
setPasswordResetToken(email) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import authDataManager from '../Adapters/Auth';
|
||||
import { ParseServerOptions } from '../Options';
|
||||
import { loadAdapter } from '../Adapters/AdapterLoader';
|
||||
import { loadAdapter, loadModule } from '../Adapters/AdapterLoader';
|
||||
import defaults from '../defaults';
|
||||
// Controllers
|
||||
import { LoggerController } from './LoggerController';
|
||||
@@ -22,7 +22,6 @@ import { InMemoryCacheAdapter } from '../Adapters/Cache/InMemoryCacheAdapter';
|
||||
import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter';
|
||||
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter';
|
||||
import ParsePushAdapter from '@parse/push-adapter';
|
||||
import ParseGraphQLController from './ParseGraphQLController';
|
||||
import SchemaCache from '../Adapters/Cache/SchemaCache';
|
||||
|
||||
@@ -30,13 +29,6 @@ export function getControllers(options: ParseServerOptions) {
|
||||
const loggerController = getLoggerController(options);
|
||||
const filesController = getFilesController(options);
|
||||
const userController = getUserController(options);
|
||||
const {
|
||||
pushController,
|
||||
hasPushScheduledSupport,
|
||||
hasPushSupport,
|
||||
pushControllerQueue,
|
||||
pushWorker,
|
||||
} = getPushController(options);
|
||||
const cacheController = getCacheController(options);
|
||||
const analyticsController = getAnalyticsController(options);
|
||||
const liveQueryController = getLiveQueryController(options);
|
||||
@@ -51,11 +43,6 @@ export function getControllers(options: ParseServerOptions) {
|
||||
loggerController,
|
||||
filesController,
|
||||
userController,
|
||||
pushController,
|
||||
hasPushScheduledSupport,
|
||||
hasPushSupport,
|
||||
pushWorker,
|
||||
pushControllerQueue,
|
||||
analyticsController,
|
||||
cacheController,
|
||||
parseGraphQLController,
|
||||
@@ -182,7 +169,7 @@ interface PushControlling {
|
||||
pushWorker: PushWorker;
|
||||
}
|
||||
|
||||
export function getPushController(options: ParseServerOptions): PushControlling {
|
||||
export async function getPushController(options: ParseServerOptions): PushControlling {
|
||||
const { scheduledPush, push } = options;
|
||||
|
||||
const pushOptions = Object.assign({}, push);
|
||||
@@ -192,6 +179,7 @@ export function getPushController(options: ParseServerOptions): PushControlling
|
||||
}
|
||||
|
||||
// Pass the push options too as it works with the default
|
||||
const ParsePushAdapter = await loadModule('@parse/push-adapter');
|
||||
const pushAdapter = loadAdapter(
|
||||
pushOptions && pushOptions.adapter,
|
||||
ParsePushAdapter,
|
||||
|
||||
@@ -15,7 +15,4 @@
|
||||
*
|
||||
* If there are no deprecations, this must return an empty array.
|
||||
*/
|
||||
module.exports = [
|
||||
{ optionKey: 'allowClientClassCreation', changeNewDefault: 'false' },
|
||||
{ optionKey: 'allowExpiredAuthDataToken', changeNewDefault: 'false' },
|
||||
];
|
||||
module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }];
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import corsMiddleware from 'cors';
|
||||
import { createServer, renderGraphiQL } from '@graphql-yoga/node';
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js';
|
||||
import { ApolloServer } from '@apollo/server';
|
||||
import { expressMiddleware } from '@apollo/server/express4';
|
||||
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
|
||||
import express from 'express';
|
||||
import { execute, subscribe } from 'graphql';
|
||||
import { SubscriptionServer } from 'subscriptions-transport-ws';
|
||||
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
|
||||
@@ -33,16 +37,13 @@ class ParseGraphQLServer {
|
||||
try {
|
||||
return {
|
||||
schema: await this.parseGraphQLSchema.load(),
|
||||
context: ({ req: { info, config, auth } }) => ({
|
||||
info,
|
||||
config,
|
||||
auth,
|
||||
}),
|
||||
maskedErrors: false,
|
||||
multipart: {
|
||||
fileSize: this._transformMaxUploadSizeToBytes(
|
||||
this.parseServer.config.maxUploadSize || '20mb'
|
||||
),
|
||||
context: async ({ req, res }) => {
|
||||
res.set('access-control-allow-origin', req.get('origin') || '*');
|
||||
return {
|
||||
info: req.info,
|
||||
config: req.config,
|
||||
auth: req.auth,
|
||||
};
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -57,8 +58,21 @@ class ParseGraphQLServer {
|
||||
if (schemaRef === newSchemaRef && this._server) {
|
||||
return this._server;
|
||||
}
|
||||
const options = await this._getGraphQLOptions();
|
||||
this._server = createServer(options);
|
||||
const { schema, context } = await this._getGraphQLOptions();
|
||||
const apollo = new ApolloServer({
|
||||
csrfPrevention: {
|
||||
// See https://www.apollographql.com/docs/router/configuration/csrf/
|
||||
// needed since we use graphql upload
|
||||
requestHeaders: ['X-Parse-Application-Id'],
|
||||
},
|
||||
introspection: true,
|
||||
plugins: [ApolloServerPluginCacheControlDisabled()],
|
||||
schema,
|
||||
});
|
||||
await apollo.start();
|
||||
this._server = expressMiddleware(apollo, {
|
||||
context,
|
||||
});
|
||||
return this._server;
|
||||
}
|
||||
|
||||
@@ -79,14 +93,21 @@ class ParseGraphQLServer {
|
||||
if (!app || !app.use) {
|
||||
requiredParameter('You must provide an Express.js app instance!');
|
||||
}
|
||||
|
||||
app.use(this.config.graphQLPath, corsMiddleware());
|
||||
app.use(this.config.graphQLPath, handleParseHeaders);
|
||||
app.use(this.config.graphQLPath, handleParseSession);
|
||||
app.use(this.config.graphQLPath, handleParseErrors);
|
||||
app.use(this.config.graphQLPath, async (req, res) => {
|
||||
app.use(
|
||||
this.config.graphQLPath,
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: this._transformMaxUploadSizeToBytes(
|
||||
this.parseServer.config.maxUploadSize || '20mb'
|
||||
),
|
||||
})
|
||||
);
|
||||
app.use(this.config.graphQLPath, express.json(), async (req, res, next) => {
|
||||
const server = await this._getServer();
|
||||
return server(req, res);
|
||||
return server(req, res, next);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,20 +115,33 @@ class ParseGraphQLServer {
|
||||
if (!app || !app.get) {
|
||||
requiredParameter('You must provide an Express.js app instance!');
|
||||
}
|
||||
|
||||
app.get(
|
||||
this.config.playgroundPath ||
|
||||
requiredParameter('You must provide a config.playgroundPath to applyPlayground!'),
|
||||
(_req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.write(
|
||||
renderGraphiQL({
|
||||
endpoint: this.config.graphQLPath,
|
||||
subscriptionEndpoint: this.config.subscriptionsPath,
|
||||
headers: JSON.stringify({
|
||||
'X-Parse-Application-Id': this.parseServer.config.appId,
|
||||
'X-Parse-Master-Key': this.parseServer.config.masterKey,
|
||||
}),
|
||||
})
|
||||
`<div id="sandbox" style="position:absolute;top:0;right:0;bottom:0;left:0"></div>
|
||||
<script src="https://embeddable-sandbox.cdn.apollographql.com/_latest/embeddable-sandbox.umd.production.min.js"></script>
|
||||
<script>
|
||||
new window.EmbeddedSandbox({
|
||||
target: "#sandbox",
|
||||
endpointIsEditable: false,
|
||||
initialEndpoint: "${JSON.stringify(this.config.graphQLPath)}",
|
||||
handleRequest: (endpointUrl, options) => {
|
||||
return fetch(endpointUrl, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
'X-Parse-Application-Id': "${JSON.stringify(this.parseServer.config.appId)}",
|
||||
'X-Parse-Master-Key': "${JSON.stringify(this.parseServer.config.masterKey)}",
|
||||
},
|
||||
})
|
||||
},
|
||||
});
|
||||
// advanced options: https://www.apollographql.com/docs/studio/explorer/sandbox#embedding-sandbox
|
||||
</script>`
|
||||
);
|
||||
res.end();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
GraphQLUnionType,
|
||||
} from 'graphql';
|
||||
import { toGlobalId } from 'graphql-relay';
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.js';
|
||||
|
||||
class TypeValidationError extends Error {
|
||||
constructor(value, type) {
|
||||
@@ -222,11 +223,6 @@ const DATE = new GraphQLScalarType({
|
||||
},
|
||||
});
|
||||
|
||||
const GraphQLUpload = new GraphQLScalarType({
|
||||
name: 'Upload',
|
||||
description: 'The Upload scalar type represents a file upload.',
|
||||
});
|
||||
|
||||
const BYTES = new GraphQLScalarType({
|
||||
name: 'Bytes',
|
||||
description:
|
||||
|
||||
@@ -1,33 +1,61 @@
|
||||
import { GraphQLNonNull } from 'graphql';
|
||||
import { request } from 'http';
|
||||
import { getExtension } from 'mime';
|
||||
import { mutationWithClientMutationId } from 'graphql-relay';
|
||||
import Parse from 'parse/node';
|
||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||
import logger from '../../logger';
|
||||
|
||||
// Handle GraphQL file upload and proxy file upload to GraphQL server url specified in config;
|
||||
// `createFile` is not directly called by Parse Server to leverage standard file upload mechanism
|
||||
const handleUpload = async (upload, config) => {
|
||||
const data = Buffer.from(await upload.arrayBuffer());
|
||||
const fileName = upload.name;
|
||||
const type = upload.type;
|
||||
|
||||
if (!data || !data.length) {
|
||||
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.');
|
||||
}
|
||||
|
||||
if (fileName.length > 128) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.');
|
||||
}
|
||||
|
||||
if (!fileName.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename contains invalid characters.');
|
||||
}
|
||||
|
||||
const { createReadStream, filename, mimetype } = await upload;
|
||||
const headers = { ...config.headers };
|
||||
delete headers['accept-encoding'];
|
||||
delete headers['accept'];
|
||||
delete headers['connection'];
|
||||
delete headers['host'];
|
||||
delete headers['content-length'];
|
||||
const stream = createReadStream();
|
||||
try {
|
||||
const ext = getExtension(mimetype);
|
||||
const fullFileName = filename.endsWith(`.${ext}`) ? filename : `${filename}.${ext}`;
|
||||
const serverUrl = new URL(config.serverURL);
|
||||
const fileInfo = await new Promise((resolve, reject) => {
|
||||
const req = request(
|
||||
{
|
||||
hostname: serverUrl.hostname,
|
||||
port: serverUrl.port,
|
||||
path: `${serverUrl.pathname}/files/${fullFileName}`,
|
||||
method: 'POST',
|
||||
headers,
|
||||
},
|
||||
res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
reject(new Parse.Error(Parse.error, data));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
stream.pipe(req);
|
||||
stream.on('end', () => {
|
||||
req.end();
|
||||
});
|
||||
});
|
||||
return {
|
||||
fileInfo: await config.filesController.createFile(config, fileName, data, type),
|
||||
fileInfo,
|
||||
};
|
||||
} catch (e) {
|
||||
stream.destroy();
|
||||
logger.error('Error creating a file: ', e);
|
||||
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `Could not store file: ${fileName}.`);
|
||||
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `Could not store file: ${filename}.`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG
|
||||
const parseFields = await transformTypes('create', fields, {
|
||||
className,
|
||||
parseGraphQLSchema,
|
||||
originalFields: args.fields,
|
||||
req: { config, auth, info },
|
||||
});
|
||||
|
||||
@@ -190,6 +191,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG
|
||||
const parseFields = await transformTypes('update', fields, {
|
||||
className,
|
||||
parseGraphQLSchema,
|
||||
originalFields: args.fields,
|
||||
req: { config, auth, info },
|
||||
});
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ const load = parseGraphQLSchema => {
|
||||
const parseFields = await transformTypes('create', fields, {
|
||||
className: '_User',
|
||||
parseGraphQLSchema,
|
||||
originalFields: args.fields,
|
||||
req: { config, auth, info },
|
||||
});
|
||||
|
||||
@@ -114,6 +115,7 @@ const load = parseGraphQLSchema => {
|
||||
const parseFields = await transformTypes('create', fields, {
|
||||
className: '_User',
|
||||
parseGraphQLSchema,
|
||||
originalFields: args.fields,
|
||||
req: { config, auth, info },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parse from 'parse/node';
|
||||
import { GraphQLYogaError } from '@graphql-yoga/node';
|
||||
import { GraphQLError } from 'graphql';
|
||||
|
||||
export function enforceMasterKeyAccess(auth) {
|
||||
if (!auth.isMaster) {
|
||||
@@ -16,7 +16,7 @@ export function toGraphQLError(error) {
|
||||
code = Parse.Error.INTERNAL_SERVER_ERROR;
|
||||
message = 'Internal server error';
|
||||
}
|
||||
return new GraphQLYogaError(message, { code });
|
||||
return new GraphQLError(message, { extensions: { code } });
|
||||
}
|
||||
|
||||
export const extractKeysAndInclude = selectedFields => {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import Parse from 'parse/node';
|
||||
import { fromGlobalId } from 'graphql-relay';
|
||||
import { handleUpload } from '../loaders/filesMutations';
|
||||
import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes';
|
||||
import * as objectsMutations from '../helpers/objectsMutations';
|
||||
|
||||
const transformTypes = async (
|
||||
inputType: 'create' | 'update',
|
||||
fields,
|
||||
{ className, parseGraphQLSchema, req }
|
||||
{ className, parseGraphQLSchema, req, originalFields }
|
||||
) => {
|
||||
const {
|
||||
classGraphQLCreateType,
|
||||
@@ -28,34 +27,38 @@ const transformTypes = async (
|
||||
inputTypeField = classGraphQLUpdateTypeFields[field];
|
||||
}
|
||||
if (inputTypeField) {
|
||||
switch (true) {
|
||||
case inputTypeField.type === defaultGraphQLTypes.GEO_POINT_INPUT:
|
||||
const parseFieldType = parseClass.fields[field].type;
|
||||
switch (parseFieldType) {
|
||||
case 'GeoPoint':
|
||||
if (fields[field] === null) {
|
||||
fields[field] = { __op: 'Delete' };
|
||||
break;
|
||||
}
|
||||
fields[field] = transformers.geoPoint(fields[field]);
|
||||
break;
|
||||
case inputTypeField.type === defaultGraphQLTypes.POLYGON_INPUT:
|
||||
case 'Polygon':
|
||||
if (fields[field] === null) {
|
||||
fields[field] = { __op: 'Delete' };
|
||||
break;
|
||||
}
|
||||
fields[field] = transformers.polygon(fields[field]);
|
||||
break;
|
||||
case inputTypeField.type === defaultGraphQLTypes.FILE_INPUT:
|
||||
fields[field] = await transformers.file(fields[field], req);
|
||||
case 'File':
|
||||
// We need to use the originalFields to handle the file upload
|
||||
// since fields are a deepcopy and do not keep the file object
|
||||
fields[field] = await transformers.file(originalFields[field], req);
|
||||
break;
|
||||
case parseClass.fields[field].type === 'Relation':
|
||||
case 'Relation':
|
||||
fields[field] = await transformers.relation(
|
||||
parseClass.fields[field].targetClass,
|
||||
field,
|
||||
fields[field],
|
||||
originalFields[field],
|
||||
parseGraphQLSchema,
|
||||
req
|
||||
);
|
||||
break;
|
||||
case parseClass.fields[field].type === 'Pointer':
|
||||
case 'Pointer':
|
||||
if (fields[field] === null) {
|
||||
fields[field] = { __op: 'Delete' };
|
||||
break;
|
||||
@@ -64,6 +67,7 @@ const transformTypes = async (
|
||||
parseClass.fields[field].targetClass,
|
||||
field,
|
||||
fields[field],
|
||||
originalFields[field],
|
||||
parseGraphQLSchema,
|
||||
req
|
||||
);
|
||||
@@ -135,7 +139,14 @@ const transformers = {
|
||||
}
|
||||
return parseACL;
|
||||
},
|
||||
relation: async (targetClass, field, value, parseGraphQLSchema, { config, auth, info }) => {
|
||||
relation: async (
|
||||
targetClass,
|
||||
field,
|
||||
value,
|
||||
originalValue,
|
||||
parseGraphQLSchema,
|
||||
{ config, auth, info }
|
||||
) => {
|
||||
if (Object.keys(value).length === 0)
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_POINTER,
|
||||
@@ -151,9 +162,10 @@ const transformers = {
|
||||
if (value.createAndAdd) {
|
||||
nestedObjectsToAdd = (
|
||||
await Promise.all(
|
||||
value.createAndAdd.map(async input => {
|
||||
value.createAndAdd.map(async (input, i) => {
|
||||
const parseFields = await transformTypes('create', input, {
|
||||
className: targetClass,
|
||||
originalFields: originalValue.createAndAdd[i],
|
||||
parseGraphQLSchema,
|
||||
req: { config, auth, info },
|
||||
});
|
||||
@@ -204,7 +216,14 @@ const transformers = {
|
||||
}
|
||||
return op;
|
||||
},
|
||||
pointer: async (targetClass, field, value, parseGraphQLSchema, { config, auth, info }) => {
|
||||
pointer: async (
|
||||
targetClass,
|
||||
field,
|
||||
value,
|
||||
originalValue,
|
||||
parseGraphQLSchema,
|
||||
{ config, auth, info }
|
||||
) => {
|
||||
if (Object.keys(value).length > 1 || Object.keys(value).length === 0)
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_POINTER,
|
||||
@@ -216,6 +235,7 @@ const transformers = {
|
||||
const parseFields = await transformTypes('create', value.createAndLink, {
|
||||
className: targetClass,
|
||||
parseGraphQLSchema,
|
||||
originalFields: originalValue.createAndLink,
|
||||
req: { config, auth, info },
|
||||
});
|
||||
nestedObjectToAdd = await objectsMutations.createObject(
|
||||
|
||||
@@ -23,7 +23,6 @@ import { LRUCache as LRU } from 'lru-cache';
|
||||
import UserRouter from '../Routers/UsersRouter';
|
||||
import DatabaseController from '../Controllers/DatabaseController';
|
||||
import { isDeepStrictEqual } from 'util';
|
||||
import Deprecator from '../Deprecator/Deprecator';
|
||||
import deepcopy from 'deepcopy';
|
||||
|
||||
class ParseLiveQueryServer {
|
||||
@@ -920,13 +919,6 @@ class ParseLiveQueryServer {
|
||||
? request.query.keys
|
||||
: request.query.keys.split(',');
|
||||
}
|
||||
if (request.query.fields) {
|
||||
subscriptionInfo.keys = request.query.fields;
|
||||
Deprecator.logRuntimeDeprecation({
|
||||
usage: `Subscribing using fields parameter`,
|
||||
solution: `Subscribe using "keys" instead.`,
|
||||
});
|
||||
}
|
||||
if (request.query.watch) {
|
||||
subscriptionInfo.watch = request.query.watch;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user