Compare commits

..
38 Commits
Author SHA1 Message Date
Florent Vilmart 0faaec3224 Release notes for 2.2.23 (#2895) 2016-10-19 18:01:12 -07:00
Marian Ignev 021fe4d336 Add SashiDo Migration guides to README (#2896) 2016-10-19 20:56:43 -04:00
Florent Vilmart 5dc24334a5 Adds ability to run livequery server on different port (appengine) (#2892)
* Adds ability to run livequery server on different port (appengine)
2016-10-19 17:11:25 -07:00
Florent Vilmart 86f35014f4 Fixes #2780 (#2810)
* Adds regression test for #2780

* Fixes #2780

- Make sure we compare installationId from the data and not the auth when rejecting update
2016-10-19 19:54:19 -04:00
Yuki Takeichi 758975cf03 Revert " Saved filename fixed so that clients display it correctly #2415 (#2705)" (#2833)
This reverts commit 22c1a87d80.
2016-10-19 16:55:20 -04:00
Jeremy Louie a6a6f7ff60 Fix error when updating installation with useMasterKey (#2888)
* Add failing test for updating installations with masterKey

* Prevent auth.installationId from being used when using masterKey

This allows masterKey to update any installation object
Fixes ParsePlatform/parse-server##2887
2016-10-19 15:06:19 -04:00
Eugene Antropov 305b037176 Vkontakte Auth: Change users.get to secure.checkToken (#2880)
* Change users.get to secure.checkToken

You can't get user info by client token due vk restrictions. You must check token via secure.checkToken.

* Configuration checks for vk auth.

* Move config check to promise, remove debug log, add message to logger on error.
2016-10-19 10:38:01 -04:00
Jeremy Louie b88b0c578f Fix multiple use of notEqualTo (#2882)
* Add failing test for multiple .notEqualTo on relation with same class

* Fix multiple .notEqualTo on relations with the same class

Multiple  should use the union of all objectIds not the intersect
Fixes ParsePlatform/parse-server#1596
2016-10-18 16:44:47 -04:00
Florent Vilmart d8ba9e8b7d Adds validation for id_token and access_token (#2878)
* ADds validation for id_token and access_token

* nit
2016-10-17 12:44:24 -04:00
Greenkeeper 60d506615d chore(package): update cross-env to version 3.1.3 (#2870)
https://greenkeeper.io/
2016-10-17 10:28:35 -04:00
Kulshekhar Kabra de36d9640b Allow configuration options for Postgres (#2873)
* Allow configuration options for Postgres

* Fix the use of incorrect options object.

* Refactor and test the postgres config parser.

* Remove unnecessary try/catch

* Remove unnecessary try/catch

* Add blank line at the end of the test file

* Rename file for consistency purposes
2016-10-15 17:29:54 -04:00
Kulshekhar Kabra 7af320932a Use the Postgres Adapter with a Postgres URI (#2871)
* Use the Postgres Adapter with a Postgres URI

* Handle malformed databaseURI
2016-10-15 10:22:49 -04:00
Florent Vilmart c253daa9c9 Update bootstrap.sh (#2862)
* Update bootstrap.sh

Fix issue when generating config.json

* Update bootstrap.sh

Fix mongodb URI
2016-10-13 13:05:36 -04:00
Greenkeeper 09f871b786 chore(package): update cross-env to version 3.1.1 (#2824)
https://greenkeeper.io/
2016-10-07 09:36:48 -04:00
Greenkeeper c0c516271c chore(package): update lodash to version 4.16.4 (#2835)
https://greenkeeper.io/
2016-10-07 09:36:11 -04:00
Greenkeeper 2cea0030f3 chore(package): update nodemon to version 1.11.0 (#2839)
https://greenkeeper.io/
2016-10-07 09:36:04 -04:00
Florent Vilmart 6979bb430b Properly handle select/include pairs #2786 (#2809)
* Regression test for 2786

* Fix issue affecting selcting/including keys
2016-10-07 09:33:55 -04:00
Greenkeeper d8f0f9e158 chore(package): update lodash to version 4.16.2 (#2779)
https://greenkeeper.io/
2016-09-27 08:53:24 -04:00
Greenkeeper 27b135b1c2 chore(package): update parse to version 1.9.2 (#2788)
https://greenkeeper.io/
2016-09-27 08:53:17 -04:00
Florent Vilmart 954a8a4159 Better support for null values in arrays (#2777)
* Adds reproduction for #2752

* Make sure we support null values in arrays of pointers for inclusion
2016-09-26 13:26:06 -04:00
Florent Vilmart a60935dbfb Changelog for 2.2.22 (#2775)
* Changelog for 2.2.22

* Update CHANGELOG.md
2016-09-24 18:36:45 -04:00
Greenkeeper 509f90b937 chore(package): update cross-env to version 3.0.0 (#2772)
https://greenkeeper.io/
2016-09-24 15:25:30 -04:00
Florent Vilmart 300150e50f Fix expports problem (#2774) 2016-09-24 15:25:22 -04:00
Florent Vilmart b5af4d017c Makes sure pointer includes are interpolated as get (#2747)
* test for repro #2005

* Adds ability to override CLP op from RestQuery.execute

* nits
2016-09-24 14:54:31 -04:00
Florent Vilmart 9260e8b05f Bumps minimum engine to 4.5 (#2733) 2016-09-24 13:53:31 -04:00
Florent Vilmart 2ec2bb5660 Removes dependency upon babel-polyfills (#2731)
* Removes runtime dependency babel-polyfill (#2692)

* Removes runtime dependency babel-polyfill

* removes references to polyfilled array includes

* Better support for polyfilling

* Removes unnecessary log

* Adds killswitch if tests are polyfilled

* Reverts usage of includes on strings
2016-09-24 13:53:15 -04:00
Florent Vilmart 3ba6e613b3 Adds repro for #1931 (#2735) 2016-09-24 13:53:04 -04:00
Florent Vilmart 4dcefd6386 Repro for issue #1944 (#2736) 2016-09-24 13:47:44 -04:00
Florent Vilmart 9c522be00d Support for nested .select() calls (#2737)
* Reproduction for #1567

* Recursive handling of nested pointer keys in select

* Better support for multi-level nested keys

* Adds support for selecting columns natively (mongo)

* Support for postgres column selections

* Filter-out empty keys for pg
2016-09-24 13:43:49 -04:00
Florent Vilmart 4974dbea37 Adds ability to ask for installation directory (#2746) 2016-09-24 13:42:51 -04:00
Florent Vilmart d4593931db Makes sure we plumb auth.installationId when updating installations (#2739) 2016-09-24 13:42:36 -04:00
Florent Vilmart 2183b84565 CLI for parse-live-query-server (#2765)
* adds CLI for parse-live-query-server, adds ability to start parse-server with live-query server

* Don't crash when the message is badly formatted
2016-09-24 13:34:05 -04:00
Arthur Cinader a41cbcbc7f Move password masking out of logging clients where possible (#2762)
Move password masking functionality into LoggerController.

The is a more aggresive approach to masking password string in the logs.

Cleaning the url is still in the PromiseRouter because picking it out of the log string
would be fragile.

This will cause more log messages to be scanned for password strings, and may cause a password
string to be obsfucated that is not neccesarily part of parse internals -- but i think that is
still a good thing....

see: #2755 & #2680
2016-09-22 15:05:54 -04:00
Florent Vilmart ad707457be Obfuscate password name value pairs in log strings (#2755)
* Unit test to catch password in logs.

* Add clean to logger controller to "look for" password strings in log messages.
2016-09-20 21:45:24 -04:00
Greenkeeper 5feceaa6d6 chore(package): update lodash to version 4.16.0 (#2742)
https://greenkeeper.io/
2016-09-19 15:15:10 -04:00
Florent Vilmart e30989c7d3 Lookup for email in username field to match docs if email is undefined (#2732)
* Lookup for email in username field to match docs if email is undefined

* Adds support for sendMail option to when email is selected

* Proper does not exists clause
2016-09-18 18:32:34 -04:00
Florent Vilmart 7e037ffa72 version 2.2.21 (#2730) 2016-09-18 13:03:18 -04:00
Florent Vilmart 0ec1e8ca7f Revert "Removes runtime dependency babel-polyfill" (#2729) 2016-09-18 12:47:42 -04:00
46 changed files with 1231 additions and 298 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
language: node_js
node_js:
- '4.3'
- '4.5'
- '6.1'
services:
- postgresql
+37
View File
@@ -1,5 +1,42 @@
## Parse Server Changelog
### 2.2.23
* Run liveQuery server from CLI with a different port, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Support for Postgres databaseURI, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Support for Postgres options, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Improved support for google login (id_token and access_token), thanks to [Florent Vilmart](https://github.com/flovilmart)
* Improvements with VKontakte login, thanks to [Eugene Antropov](https://github.com/antigp)
* Improved support for `select` and `include`, thanks to [Florent Vilmart](https://github.com/flovilmart)
#### Bug fixes
* Fix error when updating installation with useMasterKey (#2888), thanks to [Jeremy Louie](https://github.com/JeremyPlease)
* Fix bug affecting usage of multiple `notEqualTo`, thanks to [Jeremy Louie](https://github.com/JeremyPlease)
* Improved support for null values in arrays, thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.2.22
* Minimum nodejs engine is now 4.5
#### New Features
* New: CLI for parse-live-query-server, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: Start parse-live-query-server for parse-server CLI, thanks to [Florent Vilmart](https://github.com/flovilmart)
#### Bug fixes
* Fix: Include with pointers are not conflicting with get CLP anymore, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Removes dependency on babel-polyfill, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Support nested select calls, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Use native column selection instead of runtime, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: installationId header is properly used when updating `_Installation` objects, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: don't crash parse-server on improperly formatted live-query messages, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Passwords are properly stripped out of logs, thanks to [Arthur Cinader](https://github.com/acinader)
* Fix: Lookup for email in username if email is not set, thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.2.21
* Fix: Reverts removal of babel-polyfill
### 2.2.20
* New: Adds CloudCode handler for `beforeFind`, thanks to [Florent Vilmart](https://github.com/flovilmart)
+3 -2
View File
@@ -110,10 +110,11 @@ We have provided a basic [Node.js application](https://github.com/ParsePlatform/
* [Heroku and mLab](https://devcenter.heroku.com/articles/deploying-a-parse-server-to-heroku)
* [AWS and Elastic Beanstalk](http://mobile.awsblog.com/post/TxCD57GZLM2JR/How-to-set-up-Parse-Server-on-AWS-using-AWS-Elastic-Beanstalk)
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
* [NodeChef](https://nodechef.com/blog/post/6/migrate-from-parse-to-nodechef%E2%80%99s-managed-parse-server)
* [Google App Engine](https://medium.com/@justinbeckwith/deploying-parse-server-to-google-app-engine-6bc0b7451d50)
* [Microsoft Azure](https://azure.microsoft.com/en-us/blog/azure-welcomes-parse-developers/)
* [SashiDo](https://blog.sashido.io/tag/migration/)
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
* [NodeChef](https://nodechef.com/blog/post/6/migrate-from-parse-to-nodechef%E2%80%99s-managed-parse-server)
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
* [HyperDev](https://hyperdev.com/blog/use-parse-server-apps-backend-hyperdev/)
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
require("../lib/cli/parse-live-query-server");
+60 -14
View File
@@ -4,7 +4,7 @@ GREEN='\033[0;32m'
NC='\033[0m'
BOLD='\033[1m'
CHECK="${GREEN}\xE2\x9C\x93${NC}"
DEFAULT_MONGODB_URI='mongodb://localhost:127.0.0.1:27017/parse'
DEFAULT_MONGODB_URI='mongodb://127.0.0.1:27017/parse'
confirm() {
DEFAULT=$1;
@@ -53,15 +53,60 @@ check_npm() {
echo ''
echo 'This will setup parse-server in the current directory'
echo '
`.-://////:-..`
`:/oooooooooooooooo+:.`
`:+oooooooooooooooooooooo+/`
:+ooooooooooooooooooooooooooo/.
.+oooooooooooooo/:.....-:+ooooooo-
.+ooooooooooooo/` .:///:-` -+oooooo:
`+ooooooooooooo: `/ooooooo+- `ooooooo-
:oooooooooooooo :ooooooooo+` /oooooo+
+ooooooooooooo/ +ooooooooo+ /ooooooo.
oooooooooooooo+ ooooooooo` .oooooooo.
+ooooooooooo+/: `ooooooo` .:ooooooooo.
:ooooooo+.````````````` /+oooooooooo+
`+oooooo- `ooo+ /oooooooooooooooooooo-
.+ooooo/ :/:` -ooooooooooooooooooo:
.+ooooo+:-..-/ooooooooooooooooooo-
:+ooooooooooooooooooooooooooo/.
`:+oooooooooooooooooooooo+/`
`:/oooooooooooooooo+:.`
`.-://////:-..`
parse-server
'
INSTALL_DIR=""
printf "Enter an installation directory\n"
printf "(%s): " "${PWD}"
read -r INSTALL_DIR
if [ "$INSTALL_DIR" = "" ]; then
INSTALL_DIR="${PWD}"
fi
echo ''
printf "This will setup parse-server in %s\n" "${INSTALL_DIR}"
confirm 'Y' 'Do you want to continue? (Y/n): '
check_node
check_npm
echo "Setting up parse-server in $PWD"
printf "Setting up parse-server in %s" "${INSTALL_DIR}\n"
if [ -f './package.json' ]; then
if [ -d "${INSTALL_DIR}" ]; then
echo "{CHECK} ${INSTALL_DIR} exists"
else
mkdir -p "${INSTALL_DIR}"
echo "${CHECK} Created ${INSTALL_DIR}"
fi
cd "${INSTALL_DIR}"
if [ -f "package.json" ]; then
echo "\n${RED}package.json exists${NC}"
confirm 'N' "Do you want to continue? \n${RED}this will erase your configuration${NC} (y/N): "
fi
@@ -77,33 +122,33 @@ i=0
while [ "$APP_NAME" = "" ]
do
[[ $i != 0 ]] && printf "${RED}An application name is required!${NC}\n"
printf 'Enter your Application Name: '
printf "Enter your ${BOLD}Application Name${NC}: "
read -r APP_NAME
i=$(($i+1))
done
printf 'Enter your appId (leave empty to generate): '
printf "Enter your ${BOLD}Application Id${NC} (leave empty to generate): "
read -r APP_ID
[[ $APP_ID = '' ]] && APP_ID=$(genstring) && printf "\n$APP_ID\n\n"
printf 'Enter your masterKey (leave empty to generate): '
printf "Enter your ${BOLD}Master Key${NC} (leave empty to generate): "
read -r MASTER_KEY
[[ $MASTER_KEY = '' ]] && MASTER_KEY=$(genstring) && printf "\n$MASTER_KEY\n\n"
printf "Enter your mongodbURI (%s): " $DEFAULT_MONGODB_URI
printf "Enter your ${BOLD}mongodbURI${NC} (%s): " $DEFAULT_MONGODB_URI
read -r MONGODB_URI
[[ $MONGODB_URI = '' ]] && MONGODB_URI="$DEFAULT_MONGODB_URI"
cat > ./config.json << EOF
{
"appId": "$APP_ID",
"masterKey": "$MASTER_KEY",
"appName": "$APP_NAME",
"appId": "${APP_ID}",
"masterKey": "${MASTER_KEY}",
"appName": "${APP_NAME}",
"cloud": "./cloud/main",
"databaseURI": "$MONGODB_URI"
"databaseURI": "${MONGODB_URI}"
}
EOF
echo "${CHECK} Created config.json"
@@ -113,8 +158,9 @@ NPM_APP_NAME=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
cat > ./package.json << EOF
{
"name": "$NPM_APP_NAME",
"description": "parse-server for $APP_NAME",
"scripts": {
"start": "parse-server ./config.json"
"start": "parse-server config.json"
},
"dependencies": {
"parse-server": "^2.0.0"
@@ -149,7 +195,7 @@ fi
echo "\n${CHECK} running npm install\n"
npm install
npm install -s
CURL_CMD=$(cat << EOF
curl -X POST -H 'X-Parse-Application-Id: ${APP_ID}' \\
+9 -10
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.2.20",
"version": "2.2.23",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -24,13 +24,12 @@
"deepcopy": "0.6.3",
"express": "4.14.0",
"intersect": "1.0.1",
"lodash": "4.15.0",
"lodash": "4.16.4",
"lru-cache": "4.0.1",
"mime": "1.3.4",
"mongodb": "2.2.10",
"multer": "1.2.0",
"node-uuid": "^1.4.7",
"parse": "1.9.1",
"parse": "1.9.2",
"parse-server-fs-adapter": "1.0.1",
"parse-server-push-adapter": "1.1.0",
"parse-server-s3-adapter": "1.0.5",
@@ -53,27 +52,27 @@
"babel-preset-stage-0": "6.5.0",
"babel-register": "6.14.0",
"bcrypt-nodejs": "0.0.3",
"cross-env": "2.0.1",
"cross-env": "3.1.3",
"deep-diff": "0.3.4",
"gaze": "1.1.1",
"istanbul": "1.0.0-alpha.1",
"jasmine": "2.5.2",
"mongodb-runner": "3.4.0",
"nodemon": "1.10.0",
"nodemon": "1.11.0",
"request-promise": "4.1.1"
},
"scripts": {
"dev": "npm run build && node bin/dev",
"build": "babel src/ -d lib/",
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
"test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 babel-node ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
"test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 node ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
"coverage": "cross-env COVERAGE_OPTION='./node_modules/.bin/istanbul cover' npm test",
"coverage:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 babel-node ./node_modules/babel-istanbul/lib/cli.js cover ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
"coverage:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 node ./node_modules/babel-istanbul/lib/cli.js cover ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
"start": "node ./bin/parse-server",
"prepublish": "npm run build"
},
"engines": {
"node": ">=4.3"
"node": ">=4.5"
},
"bin": {
"parse-server": "./bin/parse-server"
+26
View File
@@ -712,6 +712,32 @@ describe('Cloud Code', () => {
done();
});
});
it('beforeSave change propagates through the afterSave #1931', (done) => {
Parse.Cloud.beforeSave('ChangingObject', function(request, response) {
request.object.unset('file');
request.object.unset('date');
response.success();
});
Parse.Cloud.afterSave('ChangingObject', function(request, response) {
let json = request.object.toJSON();
expect(request.object.has("file")).toBe(false);
expect(request.object.has("date")).toBe(false);
expect(request.object.get('file')).toBeUndefined();
return Promise.resolve();
});
let file = new Parse.File("yolo.txt", [1,2,3], "text/plain");
file.save().then(() => {
let obj = new Parse.Object('ChangingObject');
return obj.save({ file, date: new Date() })
}).then(() => {
done();
}, () => {
fail();
done();
})
});
it('test cloud function parameter validation success', (done) => {
// Register a function with validation
+18
View File
@@ -55,6 +55,24 @@ describe("Cloud Code Logger", () => {
});
});
it('trigger should obfuscate password', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.beforeSave(Parse.User, (req, res) => {
res.success(req.object);
});
Parse.User.signUp('tester123', 'abc')
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then((res) => {
const entry = res[0];
expect(entry.message).not.toMatch(/password":"abc/);
expect(entry.message).toMatch(/\*\*\*\*\*\*\*\*/);
done();
})
.then(null, e => done.fail(e));
});
it("should expose log to trigger", (done) => {
var logController = new LoggerController(new WinstonLoggerAdapter());
+12 -1
View File
@@ -2,9 +2,20 @@ module.exports = options => {
if (!options) {
throw "Options were not provided"
}
return {
let adapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
};
if (options.sendMail) {
adapter.sendMail = options.sendMail
}
if (options.sendPasswordResetEmail) {
adapter.sendPasswordResetEmail = options.sendPasswordResetEmail
}
if (options.sendVerificationEmail) {
adapter.sendVerificationEmail = options.sendVerificationEmail;
}
return adapter;
}
+24
View File
@@ -571,4 +571,28 @@ describe('Parse.File testing', () => {
done();
});
});
it('return with publicServerURL when provided', done => {
reconfigureServer({
publicServerURL: 'https://mydomain/parse'
}).then(() => {
var file = {
__type: 'File',
name: '123.txt'
};
var obj = new Parse.Object('FileTest');
obj.set('file', file);
return obj.save()
}).then(() => {
var query = new Parse.Query('FileTest');
return query.first();
}).then(result => {
let fileAgain = result.get('file');
expect(fileAgain.url().indexOf('https://mydomain/parse')).toBe(0);
done();
}).catch((e) => {
jfail(e);
done();
});
});
});
+111 -5
View File
@@ -37,7 +37,7 @@ describe('Installations', () => {
expect(obj.installationId).toEqual(installId);
expect(obj.deviceType).toEqual(device);
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
it('creates an ios installation with ids', (done) => {
@@ -55,7 +55,7 @@ describe('Installations', () => {
expect(obj.deviceToken).toEqual(t);
expect(obj.deviceType).toEqual(device);
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
it('creates an embedded installation with ids', (done) => {
@@ -73,7 +73,7 @@ describe('Installations', () => {
expect(obj.installationId).toEqual(installId);
expect(obj.deviceType).toEqual(device);
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
it('creates an android installation with all fields', (done) => {
@@ -96,7 +96,7 @@ describe('Installations', () => {
expect(obj.channels[0]).toEqual('foo');
expect(obj.channels[1]).toEqual('bar');
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
it('creates an ios installation with all fields', (done) => {
@@ -119,7 +119,7 @@ describe('Installations', () => {
expect(obj.channels[0]).toEqual('foo');
expect(obj.channels[1]).toEqual('bar');
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
it('should properly fail queying installations', (done) => {
@@ -872,6 +872,112 @@ describe('Installations', () => {
});
});
it('allows you to update installation from header (#2090)', done => {
let installId = '12345678-abcd-abcd-abcd-123456789abc';
let device = 'android';
let input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(createResult => {
let headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': installId
};
request.post({
headers: headers,
url: 'http://localhost:8378/1/classes/_Installation',
json: true,
body: {
date: new Date()
}
}, (error, response, body) => {
expect(response.statusCode).toBe(200);
expect(body.updatedAt).not.toBeUndefined();
done();
});
})
.catch(error => {
console.log(error);
fail('failed');
done();
});
});
it('allows you to update installation with masterKey', done => {
let installId = '12345678-abcd-abcd-abcd-123456789abc';
let device = 'android';
let input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(createResult => {
let installationObj = Parse.Installation.createWithoutData(createResult.response.objectId);
installationObj.set('customField', 'custom value');
return installationObj.save(null, {useMasterKey: true});
}).then(updateResult => {
expect(updateResult).not.toBeUndefined();
expect(updateResult.get('customField')).toEqual('custom value');
done();
}).catch(error => {
console.log(error);
fail('failed');
done();
});
});
it('should properly handle installation save #2780', done => {
let installId = '12345678-abcd-abcd-abcd-123456789abc';
let device = 'android';
let input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input).then(() => {
let query = new Parse.Query(Parse.Installation);
query.equalTo('installationId', installId);
query.first({useMasterKey: true}).then((installation) => {
return installation.save({
key: 'value'
}, {useMasterKey: true});
}).then(() => {
done();
}, (err) => {
jfail(err)
done();
});
});
});
it('should properly reject updating installationId', done => {
let installId = '12345678-abcd-abcd-abcd-123456789abc';
let device = 'android';
let input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input).then(() => {
let query = new Parse.Query(Parse.Installation);
query.equalTo('installationId', installId);
query.first({useMasterKey: true}).then((installation) => {
return installation.save({
key: 'value',
installationId: '22222222-abcd-abcd-abcd-123456789abc'
}, {useMasterKey: true});
}).then(() => {
fail('should not succeed');
done();
}, (err) => {
expect(err.code).toBe(136);
expect(err.message).toBe('installationId may not be changed in this operation');
done();
});
});
});
// TODO: Look at additional tests from installation_collection_test.go:882
// TODO: Do we need to support _tombstone disabling of installations?
// TODO: Test deletion, badge increments
+63
View File
@@ -1922,4 +1922,67 @@ describe('Parse.Object testing', () => {
done();
})
});
it('should handle includes on null arrays #2752', (done) => {
let obj1 = new Parse.Object("AnObject");
let obj2 = new Parse.Object("AnotherObject");
let obj3 = new Parse.Object("NestedObject");
obj3.set({
"foo": "bar"
})
obj2.set({
"key": obj3
})
Parse.Object.saveAll([obj1, obj2]).then(() => {
obj1.set("objects", [null, null, obj2]);
return obj1.save();
}).then(() => {
let query = new Parse.Query("AnObject");
query.include("objects.key");
return query.find();
}).then((res) => {
let obj = res[0];
expect(obj.get("objects")).not.toBe(undefined);
let array = obj.get("objects");
expect(Array.isArray(array)).toBe(true);
expect(array[0]).toBe(null);
expect(array[1]).toBe(null);
expect(array[2].get("key").get("foo")).toEqual("bar");
done();
}).catch(err => {
jfail(err);
done();
})
});
it('should handle select and include #2786', (done) => {
let score = new Parse.Object("GameScore");
let player = new Parse.Object("Player");
score.set({
"score": 1234
});
score.save().then(() => {
player.set("gameScore", score);
player.set("other", "value");
return player.save();
}).then(() => {
let query = new Parse.Query("Player");
query.include("gameScore");
query.select("gameScore");
return query.find();
}).then((res) => {
let obj = res[0];
let gameScore = obj.get("gameScore");
let other = obj.get("other");
expect(other).toBeUndefined();
expect(gameScore).not.toBeUndefined();
expect(gameScore.get("score")).toBe(1234);
done();
}).catch(err => {
jfail(err);
done();
})
});
});
+86
View File
@@ -127,6 +127,22 @@ describe('Parse.Query testing', () => {
return query.find().then(function(results){
equal(results.length, 0);
});
}).then(function(){
var query = new Parse.Query(Cake);
query.notEqualTo("hater", user2);
query.notEqualTo("liker", user2);
// user2 doesn't like any cake so this should be 0
return query.find().then(function(results){
equal(results.length, 0);
});
}).then(function(){
var query = new Parse.Query(Cake);
query.equalTo("hater", user);
query.equalTo("liker", user);
// user doesn't hate any cake so this should be 0
return query.find().then(function(results){
equal(results.length, 0);
});
}).then(function(){
done();
}).catch((err) => {
@@ -2567,6 +2583,76 @@ describe('Parse.Query testing', () => {
})
});
it('select nested keys (issue #1567)', function(done) {
var Foobar = new Parse.Object('Foobar');
var BarBaz = new Parse.Object('Barbaz');
BarBaz.set('key', 'value');
BarBaz.set('otherKey', 'value');
BarBaz.save().then(() => {
Foobar.set('foo', 'bar');
Foobar.set('fizz', 'buzz');
Foobar.set('barBaz', BarBaz);
return Foobar.save();
}).then(function(savedFoobar){
var foobarQuery = new Parse.Query('Foobar');
foobarQuery.include('barBaz');
foobarQuery.select(['fizz', 'barBaz.key']);
foobarQuery.get(savedFoobar.id,{
success: function(foobarObj){
equal(foobarObj.get('fizz'), 'buzz');
equal(foobarObj.get('foo'), undefined);
if (foobarObj.has('barBaz')) {
equal(foobarObj.get('barBaz').get('key'), 'value');
equal(foobarObj.get('barBaz').get('otherKey'), undefined);
} else {
fail('barBaz should be set');
}
done();
}
});
});
});
it('select nested keys 2 level (issue #1567)', function(done) {
var Foobar = new Parse.Object('Foobar');
var BarBaz = new Parse.Object('Barbaz');
var Bazoo = new Parse.Object('Bazoo');
Bazoo.set('some', 'thing');
Bazoo.set('otherSome', 'value');
Bazoo.save().then(() => {
BarBaz.set('key', 'value');
BarBaz.set('otherKey', 'value');
BarBaz.set('bazoo', Bazoo);
return BarBaz.save();
}).then(() => {
Foobar.set('foo', 'bar');
Foobar.set('fizz', 'buzz');
Foobar.set('barBaz', BarBaz);
return Foobar.save();
}).then(function(savedFoobar){
var foobarQuery = new Parse.Query('Foobar');
foobarQuery.include('barBaz');
foobarQuery.include('barBaz.bazoo');
foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']);
foobarQuery.get(savedFoobar.id,{
success: function(foobarObj){
equal(foobarObj.get('fizz'), 'buzz');
equal(foobarObj.get('foo'), undefined);
if (foobarObj.has('barBaz')) {
equal(foobarObj.get('barBaz').get('key'), 'value');
equal(foobarObj.get('barBaz').get('otherKey'), undefined);
equal(foobarObj.get('barBaz').get('bazoo').get('some'), 'thing');
equal(foobarObj.get('barBaz').get('bazoo').get('otherSome'), undefined);
} else {
fail('barBaz should be set');
}
done();
}
});
});
});
it('properly handles nested ors', function(done) {
var objects = [];
while(objects.length != 4) {
+65
View File
@@ -0,0 +1,65 @@
const parser = require('../src/Adapters/Storage/Postgres/PostgresConfigParser');
const queryParamTests = {
'a=1&b=2': { a: '1', b: '2' },
'a=abcd%20efgh&b=abcd%3Defgh': { a: 'abcd efgh', b: 'abcd=efgh' },
'a=1&b&c=true': { a: '1', b: '', c: 'true' }
}
describe('PostgresConfigParser.parseQueryParams', () => {
it('creates a map from a query string', () => {
for (const key in queryParamTests) {
const result = parser.parseQueryParams(key);
const testObj = queryParamTests[key];
expect(Object.keys(result).length)
.toEqual(Object.keys(testObj).length);
for (const k in result) {
expect(result[k]).toEqual(testObj[k]);
}
}
})
});
const baseURI = 'postgres://username:password@localhost:5432/db-name'
const dbOptionsTest = {};
dbOptionsTest[`${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=10`] = {
ssl: true,
binary: true,
application_name: 'app_name',
fallback_application_name: 'f_app_name',
poolSize: 10
};
dbOptionsTest[`${baseURI}?ssl=&binary=aa`] = {
ssl: false,
binary: false
}
describe('PostgresConfigParser.getDatabaseOptionsFromURI', () => {
it('creates a db options map from a query string', () => {
for (const key in dbOptionsTest) {
const result = parser.getDatabaseOptionsFromURI(key);
const testObj = dbOptionsTest[key];
for (const k in testObj) {
expect(result[k]).toEqual(testObj[k]);
}
}
});
it('sets the poolSize to 10 if the it is not a number', () => {
const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=sdf`);
expect(result.poolSize).toEqual(10);
});
});
+39
View File
@@ -479,6 +479,45 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
});
});
it('succeeds sending a password reset username if appName, publicServerURL, and email adapter are prodvided', done => {
let adapter = MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
sendMail: function(options) {
expect(options.to).toEqual('testValidConfig@parse.com');
return Promise.resolve();
}
});
// delete that handler to force using the default
delete adapter.sendPasswordResetEmail;
spyOn(adapter, 'sendMail').and.callThrough();
reconfigureServer({
appName: 'coolapp',
publicServerURL: 'http://localhost:1337/1',
emailAdapter: adapter
})
.then(() => {
let user = new Parse.User();
user.setPassword("asdf");
user.setUsername("testValidConfig@parse.com");
user.signUp(null)
.then(user => Parse.User.requestPasswordReset("testValidConfig@parse.com"))
.then(result => {
expect(adapter.sendMail).toHaveBeenCalled();
done();
}, error => {
done(error);
});
})
.catch(error => {
fail(JSON.stringify(error));
done();
});
});
it('does not send verification email if email verification is disabled', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
-16
View File
@@ -97,20 +97,4 @@ describe('verbose logs', () => {
done();
})
});
it("should not mask information in non _User class", (done) => {
let obj = new Parse.Object('users');
obj.set('password', 'pw');
obj.save().then(() => {
let winstonLoggerAdapter = new WinstonLoggerAdapter();
return winstonLoggerAdapter.query({
from: new Date(Date.now() - 500),
size: 100,
level: 'verbose'
});
}).then((results) => {
expect(results[1].body.password).toEqual("pw");
done();
});
});
});
+8 -3
View File
@@ -14,6 +14,11 @@ global.on_db = (db, callback, elseCallback) => {
}
}
if (global._babelPolyfill) {
console.error('We should not use polyfilled tests');
process.exit(1);
}
var cache = require('../src/cache').default;
var express = require('express');
var facebook = require('../src/authDataManager/facebook');
@@ -214,7 +219,7 @@ afterEach(function(done) {
} else {
// Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will
// break it.
return ['_User', '_Installation', '_Role', '_Session', '_Product'].includes(className);
return ['_User', '_Installation', '_Role', '_Session', '_Product'].indexOf(className) >= 0;
}
}});
});
@@ -387,7 +392,7 @@ global.jfail = function(err) {
}
global.it_exclude_dbs = excluded => {
if (excluded.includes(process.env.PARSE_SERVER_TEST_DB)) {
if (excluded.indexOf(process.env.PARSE_SERVER_TEST_DB) >= 0) {
return xit;
} else {
return it;
@@ -395,7 +400,7 @@ global.it_exclude_dbs = excluded => {
}
global.fit_exclude_dbs = excluded => {
if (excluded.includes(process.env.PARSE_SERVER_TEST_DB)) {
if (excluded.indexOf(process.env.PARSE_SERVER_TEST_DB) >= 0) {
return xit;
} else {
return fit;
+28
View File
@@ -1548,6 +1548,34 @@ describe('schemas', () => {
});
});
it('can query with include and CLP (issue #2005)', (done) => {
setPermissionsOnClass('AnotherObject', {
get: {"*": true},
find: {},
create: {'*': true},
update: {'*': true},
delete: {'*': true},
addField:{'*': true}
}).then(() => {
let obj = new Parse.Object('AnObject');
let anotherObject = new Parse.Object('AnotherObject');
return obj.save({
anotherObject
})
}).then(() => {
let query = new Parse.Query('AnObject');
query.include('anotherObject');
return query.find();
}).then((res) => {
expect(res.length).toBe(1);
expect(res[0].get('anotherObject')).not.toBeUndefined();
done();
}).catch((err) => {
jfail(err);
done();
})
});
it('can add field as master (issue #1257)', (done) => {
setPermissionsOnClass('AClass', {
'addField': {}
+1 -1
View File
@@ -95,5 +95,5 @@ export function removeTransport(transport) {
});
}
export { logger, addTransport, configureLogger, removeTransport };
export { logger };
export default logger;
+10 -6
View File
@@ -13,8 +13,8 @@ export default class MongoCollection {
// none, then build the geoindex.
// This could be improved a lot but it's not clear if that's a good
// idea. Or even if this behavior is a good idea.
find(query, { skip, limit, sort } = {}) {
return this._rawFind(query, { skip, limit, sort })
find(query, { skip, limit, sort, keys } = {}) {
return this._rawFind(query, { skip, limit, sort, keys })
.catch(error => {
// Check for "no geoindex" error
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
@@ -30,14 +30,18 @@ export default class MongoCollection {
index[key] = '2d';
return this._mongoCollection.createIndex(index)
// Retry, but just once.
.then(() => this._rawFind(query, { skip, limit, sort }));
.then(() => this._rawFind(query, { skip, limit, sort, keys }));
});
}
_rawFind(query, { skip, limit, sort } = {}) {
return this._mongoCollection
_rawFind(query, { skip, limit, sort, keys } = {}) {
let findOperation = this._mongoCollection
.find(query, { skip, limit, sort })
.toArray();
if (keys) {
findOperation = findOperation.project(keys);
}
return findOperation.toArray();
}
count(query, { skip, limit, sort } = {}) {
@@ -320,12 +320,16 @@ export class MongoStorageAdapter {
}
// Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }.
find(className, schema, query, { skip, limit, sort }) {
find(className, schema, query, { skip, limit, sort, keys }) {
schema = convertParseSchemaToMongoSchema(schema);
let mongoWhere = transformWhere(className, query, schema);
let mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema));
let mongoKeys = _.reduce(keys, (memo, key) => {
memo[transformKey(className, key, schema)] = 1;
return memo;
}, {});
return this._adaptiveCollection(className)
.then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort }))
.then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort, keys: mongoKeys }))
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)))
}
+5 -3
View File
@@ -11,9 +11,11 @@ const transformKey = (className, fieldName, schema) => {
case 'updatedAt': return '_updated_at';
case 'sessionToken': return '_session_token';
}
if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') {
fieldName = '_p_' + fieldName;
} else if (schema.fields[fieldName] && schema.fields[fieldName].type == 'Pointer') {
fieldName = '_p_' + fieldName;
}
return fieldName;
@@ -99,7 +101,7 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
}
const transformInteriorValue = restValue => {
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.indexOf('$') >= 0 || key.indexOf('.') >= 0)) {
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$')|| key.includes('.'))) {
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
}
// Handle atomic values
@@ -293,7 +295,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
}
// Handle normal objects by recursing
if (Object.keys(restValue).some(key => key.indexOf('$') >= 0 || key.indexOf('.') >= 0)) {
if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
}
value = _.mapValues(restValue, transformInteriorValue);
@@ -0,0 +1,26 @@
const pgp = require('pg-promise')();
const parser = require('./PostgresConfigParser');
export function createClient(uri, databaseOptions) {
let client;
let dbOptions = {};
databaseOptions = databaseOptions || {};
if (uri) {
dbOptions = parser.getDatabaseOptionsFromURI(uri);
}
for (const key in databaseOptions) {
dbOptions[key] = databaseOptions[key];
}
client = pgp(dbOptions);
if (dbOptions.pgOptions) {
for (const key in dbOptions.pgOptions) {
client.pg.defaults[key] = dbOptions.pgOptions[key];
}
}
return client;
}
@@ -0,0 +1,53 @@
const url = require('url');
function getDatabaseOptionsFromURI(uri) {
const databaseOptions = {};
const parsedURI = url.parse(uri);
const queryParams = parseQueryParams(parsedURI.query);
const authParts = parsedURI.auth ? parsedURI.auth.split(':') : [];
databaseOptions.host = parsedURI.hostname || 'localhost';
databaseOptions.port = parsedURI.port ? parseInt(parsedURI.port) : 5432;
databaseOptions.database = parsedURI.pathname
? parsedURI.pathname.substr(1)
: undefined;
databaseOptions.user = authParts.length > 0 ? authParts[0] : '';
databaseOptions.password = authParts.length > 1 ? authParts[1] : '';
databaseOptions.ssl =
queryParams.ssl && queryParams.ssl.toLowerCase() === 'true' ? true : false;
databaseOptions.binary =
queryParams.binary && queryParams.binary.toLowerCase() === 'true' ? true : false;
databaseOptions.client_encoding = queryParams.client_encoding;
databaseOptions.application_name = queryParams.application_name;
databaseOptions.fallback_application_name = queryParams.fallback_application_name;
if (queryParams.poolSize) {
databaseOptions.poolSize = parseInt(queryParams.poolSize) || 10;
}
return databaseOptions;
}
function parseQueryParams(queryString) {
queryString = queryString || '';
return queryString
.split('&')
.reduce((p, c) => {
const parts = c.split('=');
p[decodeURIComponent(parts[0])] =
parts.length > 1
? decodeURIComponent(parts.slice(1).join('='))
: '';
return p;
}, {});
}
module.exports = {
parseQueryParams: parseQueryParams,
getDatabaseOptionsFromURI: getDatabaseOptionsFromURI
};
@@ -1,4 +1,4 @@
const pgp = require('pg-promise')();
import { createClient } from './PostgresClient';
const PostgresRelationDoesNotExistError = '42P01';
const PostgresDuplicateRelationError = '42P07';
@@ -379,9 +379,10 @@ export class PostgresStorageAdapter {
constructor({
uri,
collectionPrefix = '',
databaseOptions
}) {
this._collectionPrefix = collectionPrefix;
this._client = pgp(uri);
this._client = createClient(uri, databaseOptions);
}
_ensureSchemaCollectionExists() {
@@ -415,7 +416,7 @@ export class PostgresStorageAdapter {
return toParseSchema(schema)
})
.catch((err) => {
if (err.code === PostgresUniqueIndexViolationError && err.detail.indexOf(className) >= 0) {
if (err.code === PostgresUniqueIndexViolationError && err.detail.includes(className)) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`)
}
throw err;
@@ -445,7 +446,7 @@ export class PostgresStorageAdapter {
relations.push(fieldName)
return;
}
if (['_rperm', '_wperm'].indexOf(fieldName) >=0 ) {
if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) {
parseType.contents = { type: 'String' };
}
valuesArray.push(fieldName);
@@ -921,8 +922,8 @@ export class PostgresStorageAdapter {
});
}
find(className, schema, query, { skip, limit, sort }) {
debug('find', className, query, {skip, limit, sort});
find(className, schema, query, { skip, limit, sort, keys }) {
debug('find', className, query, {skip, limit, sort, keys });
const hasLimit = limit !== undefined;
const hasSkip = skip !== undefined;
let values = [className];
@@ -954,7 +955,19 @@ export class PostgresStorageAdapter {
sortPattern = `ORDER BY ${where.sorts.join(',')}`;
}
const qs = `SELECT * FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern}`;
let columns = '*';
if (keys) {
// Exclude empty keys
keys = keys.filter((key) => {
return key.length > 0;
});
columns = keys.map((key, index) => {
return `$${index+values.length+1}:name`;
}).join(',');
values = values.concat(keys);
}
const qs = `SELECT ${columns} FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern}`;
debug(qs, values);
return this._client.any(qs, values)
.catch((err) => {
@@ -1031,9 +1044,9 @@ export class PostgresStorageAdapter {
const qs = `ALTER TABLE $1:name ADD CONSTRAINT $2:name UNIQUE (${constraintPatterns.join(',')})`;
return this._client.none(qs,[className, constraintName, ...fieldNames])
.catch(error => {
if (error.code === PostgresDuplicateRelationError && error.message.indexOf(constraintName) >= 0) {
if (error.code === PostgresDuplicateRelationError && error.message.includes(constraintName)) {
// Index already exists. Ignore error.
} else if (error.code === PostgresUniqueIndexViolationError && error.message.indexOf(constraintName) >= 0) {
} else if (error.code === PostgresUniqueIndexViolationError && error.message.includes(constraintName)) {
// Cast the error into the proper parse error
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
} else {
+22 -15
View File
@@ -44,6 +44,11 @@ const transformObjectACL = ({ ACL, ...result }) => {
}
const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count'];
const isSpecialQueryKey = key => {
return specialQuerykeys.indexOf(key) >= 0;
}
const validateQuery = query => {
if (query.ACL) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
@@ -73,7 +78,7 @@ const validateQuery = query => {
}
}
}
if (!specialQuerykeys.includes(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`);
}
});
@@ -185,6 +190,11 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
// one of the provided strings must provide the caller with
// write permissions.
const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count'];
const isSpecialUpdateKey = key => {
return specialKeysForUpdate.indexOf(key) >= 0;
}
DatabaseController.prototype.update = function(className, query, update, {
acl,
many,
@@ -227,7 +237,7 @@ DatabaseController.prototype.update = function(className, query, update, {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`);
}
fieldName = fieldName.split('.')[0];
if (!SchemaController.fieldNameIsValid(fieldName) && !specialKeysForUpdate.includes(fieldName)) {
if (!SchemaController.fieldNameIsValid(fieldName) && !isSpecialUpdateKey(fieldName)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`);
}
});
@@ -666,17 +676,12 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) {
return query;
}
DatabaseController.prototype.addNotInObjectIdsIds = function(ids = null, query) {
let idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : null;
let allIds = [idsFromNin, ids].filter(list => list !== null);
let totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
DatabaseController.prototype.addNotInObjectIdsIds = function(ids = [], query) {
let idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : [];
let allIds = [...idsFromNin,...ids].filter(list => list !== null);
let idsIntersection = [];
if (totalLength > 125) {
idsIntersection = intersect.big(allIds);
} else {
idsIntersection = intersect(allIds);
}
// make a set and spread to remove duplicates
allIds = [...new Set(allIds)];
// Need to make sure we don't clobber existing shorthand $eq constraints on objectId.
if (!('objectId' in query)) {
@@ -686,8 +691,8 @@ DatabaseController.prototype.addNotInObjectIdsIds = function(ids = null, query)
$eq: query.objectId
};
}
query.objectId['$nin'] = idsIntersection;
query.objectId['$nin'] = allIds;
return query;
}
@@ -711,10 +716,12 @@ DatabaseController.prototype.find = function(className, query, {
acl,
sort = {},
count,
keys,
op
} = {}) {
let isMaster = acl === undefined;
let aclGroup = acl || [];
let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find';
op = op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find');
let classExists = true;
return this.loadSchema()
.then(schemaController => {
@@ -779,7 +786,7 @@ DatabaseController.prototype.find = function(className, query, {
if (!classExists) {
return [];
} else {
return this.adapter.find(className, schema, query, { skip, limit, sort })
return this.adapter.find(className, schema, query, { skip, limit, sort, keys })
.then(objects => objects.map(object => {
object = untransformObjectACL(object);
return filterSensitiveData(isMaster, aclGroup, className, object)
+1 -2
View File
@@ -5,7 +5,6 @@ import AdaptableController from './AdaptableController';
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
import path from 'path';
import mime from 'mime';
import uuid from 'node-uuid';
const legacyFilesRegex = new RegExp("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-.*");
@@ -27,7 +26,7 @@ export class FilesController extends AdaptableController {
contentType = mime.lookup(filename);
}
filename = uuid.v4() + '_' + filename;
filename = randomHexString(32) + '_' + filename;
var location = this.adapter.getFileLocation(config, filename);
return this.adapter.createFile(filename, data, contentType).then(() => {
+42 -1
View File
@@ -2,6 +2,7 @@ import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import AdaptableController from './AdaptableController';
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
import url from 'url';
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
const LOG_STRING_TRUNCATE_LENGTH = 1000;
@@ -19,8 +20,48 @@ export const LogOrder = {
export class LoggerController extends AdaptableController {
maskSensitiveUrl(urlString) {
const password = url.parse(urlString, true).query.password;
if (password) {
urlString = urlString.replace('password=' + password, 'password=********');
}
return urlString;
}
maskSensitive(argArray) {
return argArray.map(e => {
if (!e) {
return e;
}
if (typeof e === 'string') {
return e.replace(/(password".?:.?")[^"]*"/g, '$1********"');
}
// else it is an object...
// check the url
if (e.url) {
e.url = this.maskSensitiveUrl(e.url);
}
if (e.body) {
for (let key of Object.keys(e.body)) {
if (key === 'password') {
e.body[key] = '********';
break;
}
}
}
return e;
});
}
log(level, args) {
args = [].concat(level, [...args]);
// make the passed in arguments object an array with the spread operator
args = this.maskSensitive([...args]);
args = [].concat(level, args);
this.adapter.log.apply(this.adapter, args);
}
+3 -4
View File
@@ -218,7 +218,7 @@ const validNonRelationOrPointerTypes = [
];
// Returns an error suitable for throwing if the type is invalid
const fieldTypeIsInvalid = ({ type, targetClass }) => {
if (['Pointer', 'Relation'].includes(type)) {
if (['Pointer', 'Relation'].indexOf(type) >= 0) {
if (!targetClass) {
return new Parse.Error(135, `type ${type} needs a class name`);
} else if (typeof targetClass !== 'string') {
@@ -232,7 +232,7 @@ const fieldTypeIsInvalid = ({ type, targetClass }) => {
if (typeof type !== 'string') {
return invalidJsonError;
}
if (!validNonRelationOrPointerTypes.includes(type)) {
if (validNonRelationOrPointerTypes.indexOf(type) < 0) {
return new Parse.Error(Parse.Error.INCORRECT_TYPE, `invalid field type: ${type}`);
}
return undefined;
@@ -520,7 +520,6 @@ export default class SchemaController {
}
})
.catch(error => {
console.error(error);
// The schema still doesn't validate. Give up
throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate');
});
@@ -541,7 +540,7 @@ export default class SchemaController {
validateSchemaData(className, fields, classLevelPermissions, existingFieldNames) {
for (let fieldName in fields) {
if (!existingFieldNames.includes(fieldName)) {
if (existingFieldNames.indexOf(fieldName) < 0) {
if (!fieldNameIsValid(fieldName)) {
return {
code: Parse.Error.INVALID_KEY_NAME,
+2 -2
View File
@@ -125,7 +125,7 @@ export class UserController extends AdaptableController {
}
setPasswordResetToken(email) {
return this.config.database.update('_User', { email }, { _perishable_token: randomString(25) }, {}, true)
return this.config.database.update('_User', { $or: [{email}, {username: email, email: {$exists: false}}] }, { _perishable_token: randomString(25) }, {}, true)
}
sendPasswordResetEmail(email) {
@@ -181,7 +181,7 @@ export class UserController extends AdaptableController {
"You requested to reset your password for " + appName + ".\n\n" +
"" +
"Click here to reset it:\n" + link;
let to = user.get("email");
let to = user.get("email") || user.get('username');
let subject = 'Password Reset for ' + appName;
return { text, to, subject };
}
+17 -5
View File
@@ -8,6 +8,7 @@ import RequestSchema from './RequestSchema';
import { matchesQuery, queryHash } from './QueryTools';
import { ParsePubSub } from './ParsePubSub';
import { SessionTokenCache } from './SessionTokenCache';
import _ from 'lodash';
class ParseLiveQueryServer {
clientId: number;
@@ -62,7 +63,13 @@ class ParseLiveQueryServer {
// to the subscribers and the handler will be called.
this.subscriber.on('message', (channel, messageStr) => {
logger.verbose('Subscribe messsage %j', messageStr);
let message = JSON.parse(messageStr);
let message;
try {
message = JSON.parse(messageStr);
} catch(e) {
logger.error('unable to parse message', messageStr, e);
return;
}
this._inflateParseObject(message);
if (channel === 'afterSave') {
this._onAfterSave(message);
@@ -116,7 +123,7 @@ class ParseLiveQueryServer {
if (!isSubscriptionMatched) {
continue;
}
for (let [clientId, requestIds] of subscription.clientRequestIds.entries()) {
for (let [clientId, requestIds] of _.entries(subscription.clientRequestIds)) {
let client = this.clients.get(clientId);
if (typeof client === 'undefined') {
continue;
@@ -159,7 +166,7 @@ class ParseLiveQueryServer {
for (let subscription of classSubscriptions.values()) {
let isOriginalSubscriptionMatched = this._matchesSubscription(originalParseObject, subscription);
let isCurrentSubscriptionMatched = this._matchesSubscription(currentParseObject, subscription);
for (let [clientId, requestIds] of subscription.clientRequestIds.entries()) {
for (let [clientId, requestIds] of _.entries(subscription.clientRequestIds)) {
let client = this.clients.get(clientId);
if (typeof client === 'undefined') {
continue;
@@ -229,7 +236,12 @@ class ParseLiveQueryServer {
_onConnect(parseWebsocket: any): void {
parseWebsocket.on('message', (request) => {
if (typeof request === 'string') {
request = JSON.parse(request);
try {
request = JSON.parse(request);
} catch(e) {
logger.error('unable to parse request', request, e);
return;
}
}
logger.verbose('Request: %j', request);
@@ -269,7 +281,7 @@ class ParseLiveQueryServer {
this.clients.delete(clientId);
// Delete client from subscriptions
for (let [requestId, subscriptionInfo] of client.subscriptionInfos.entries()) {
for (let [requestId, subscriptionInfo] of _.entries(client.subscriptionInfos)) {
let subscription = subscriptionInfo.subscription;
subscription.deleteClientSubscription(clientId, requestId);
+25 -5
View File
@@ -7,6 +7,7 @@ var batch = require('./batch'),
multer = require('multer'),
Parse = require('parse/node').Parse,
path = require('path'),
url = require('url'),
authDataManager = require('./authDataManager');
import defaults from './defaults';
@@ -54,6 +55,7 @@ import DatabaseController from './Controllers/DatabaseController';
import SchemaCache from './Controllers/SchemaCache';
import ParsePushAdapter from 'parse-server-push-adapter';
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageAdapter';
import { ParseServerRESTController } from './ParseServerRESTController';
// Mutate the Parse object to add the Cloud Code handlers
@@ -145,11 +147,7 @@ class ParseServer {
if ((databaseOptions || (databaseURI && databaseURI != defaults.DefaultMongoURI) || collectionPrefix !== '') && databaseAdapter) {
throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/collectionPrefix.';
} else if (!databaseAdapter) {
databaseAdapter = new MongoStorageAdapter({
uri: databaseURI,
collectionPrefix,
mongoOptions: databaseOptions,
});
databaseAdapter = this.getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions)
} else {
databaseAdapter = loadAdapter(databaseAdapter)
}
@@ -252,6 +250,28 @@ class ParseServer {
}
}
getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) {
let protocol;
try{
const parsedURI = url.parse(databaseURI);
protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null;
}catch(e){}
switch (protocol) {
case 'postgres:':
return new PostgresStorageAdapter({
uri: databaseURI,
collectionPrefix,
databaseOptions
});
default:
return new MongoStorageAdapter({
uri: databaseURI,
collectionPrefix,
mongoOptions: databaseOptions,
});
}
}
get app() {
return ParseServer.app(this.config);
}
+2 -22
View File
@@ -144,7 +144,7 @@ function makeExpressHandler(appId, promiseHandler) {
return function(req, res, next) {
try {
let url = maskSensitiveUrl(req);
let body = maskSensitiveBody(req);
let body = Object.assign({}, req.body);
let stringifiedBody = JSON.stringify(body, null, 2);
log.verbose(`REQUEST for [${req.method}] ${url}: ${stringifiedBody}`, {
method: req.method,
@@ -198,33 +198,13 @@ function makeExpressHandler(appId, promiseHandler) {
}
}
function maskSensitiveBody(req) {
let maskBody = Object.assign({}, req.body);
let shouldMaskBody = (req.method === 'POST' && req.originalUrl.endsWith('/users')
&& !req.originalUrl.includes('classes')) ||
(req.method === 'PUT' && /users\/\w+$/.test(req.originalUrl)
&& !req.originalUrl.includes('classes')) ||
(req.originalUrl.includes('classes/_User'));
if (shouldMaskBody) {
for (let key of Object.keys(maskBody)) {
if (key == 'password') {
maskBody[key] = '********';
break;
}
}
}
return maskBody;
}
function maskSensitiveUrl(req) {
let maskUrl = req.originalUrl.toString();
let shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login')
&& !req.originalUrl.includes('classes');
if (shouldMaskUrl) {
let password = url.parse(req.originalUrl, true).query.password;
if (password) {
maskUrl = maskUrl.replace('password=' + password, 'password=********')
}
maskUrl = log.maskSensitiveUrl(maskUrl);
}
return maskUrl;
}
+44 -27
View File
@@ -20,6 +20,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
this.auth = auth;
this.className = className;
this.restWhere = restWhere;
this.restOptions = restOptions;
this.clientSDK = clientSDK;
this.response = null;
this.findOptions = {};
@@ -56,6 +57,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
switch(option) {
case 'keys':
this.keys = new Set(restOptions.keys.split(','));
// Add the default
this.keys.add('objectId');
this.keys.add('createdAt');
this.keys.add('updatedAt');
@@ -111,11 +113,11 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
// Returns a promise for the response - an object with optional keys
// 'results' and 'count'.
// TODO: consolidate the replaceX functions
RestQuery.prototype.execute = function() {
RestQuery.prototype.execute = function(executeOptions) {
return Promise.resolve().then(() => {
return this.buildRestWhere();
}).then(() => {
return this.runFind();
return this.runFind(executeOptions);
}).then(() => {
return this.runCount();
}).then(() => {
@@ -385,13 +387,22 @@ RestQuery.prototype.replaceDontSelect = function() {
// Returns a promise for whether it was successful.
// Populates this.response with an object that only has 'results'.
RestQuery.prototype.runFind = function() {
RestQuery.prototype.runFind = function(options = {}) {
if (this.findOptions.limit === 0) {
this.response = {results: []};
return Promise.resolve();
}
let findOptions = Object.assign({}, this.findOptions);
if (this.keys) {
findOptions.keys = Array.from(this.keys).map((key) => {
return key.split('.')[0];
});
}
if (options.op) {
findOptions.op = options.op;
}
return this.config.database.find(
this.className, this.restWhere, this.findOptions).then((results) => {
this.className, this.restWhere, findOptions).then((results) => {
if (this.className === '_User') {
for (var result of results) {
delete result.password;
@@ -411,19 +422,6 @@ RestQuery.prototype.runFind = function() {
this.config.filesController.expandFilesInObject(this.config, results);
if (this.keys) {
var keySet = this.keys;
results = results.map((object) => {
var newObject = {};
for (var key in object) {
if (keySet.has(key)) {
newObject[key] = object[key];
}
}
return newObject;
});
}
if (this.redirectClassName) {
for (var r of results) {
r.className = this.redirectClassName;
@@ -455,7 +453,7 @@ RestQuery.prototype.handleInclude = function() {
}
var pathResponse = includePath(this.config, this.auth,
this.response, this.include[0]);
this.response, this.include[0], this.restOptions);
if (pathResponse.then) {
return pathResponse.then((newResponse) => {
this.response = newResponse;
@@ -473,13 +471,12 @@ RestQuery.prototype.handleInclude = function() {
// Adds included values to the response.
// Path is a list of field names.
// Returns a promise for an augmented response.
function includePath(config, auth, response, path) {
function includePath(config, auth, response, path, restOptions = {}) {
var pointers = findPointers(response.results, path);
if (pointers.length == 0) {
return response;
}
let pointersHash = {};
var objectIds = {};
for (var pointer of pointers) {
if (!pointer) {
continue;
@@ -487,15 +484,35 @@ function includePath(config, auth, response, path) {
let className = pointer.className;
// only include the good pointers
if (className) {
pointersHash[className] = pointersHash[className] || [];
pointersHash[className].push(pointer.objectId);
pointersHash[className] = pointersHash[className] || new Set();
pointersHash[className].add(pointer.objectId);
}
}
let includeRestOptions = {};
if (restOptions.keys) {
let keys = new Set(restOptions.keys.split(','));
let keySet = Array.from(keys).reduce((set, key) => {
let keyPath = key.split('.');
let i=0;
for (i; i<path.length; i++) {
if (path[i] != keyPath[i]) {
return set;
}
}
if (i < keyPath.length) {
set.add(keyPath[i]);
}
return set;
}, new Set());
if (keySet.size > 0) {
includeRestOptions.keys = Array.from(keySet).join(',');
}
}
let queryPromises = Object.keys(pointersHash).map((className) => {
var where = {'objectId': {'$in': pointersHash[className]}};
var query = new RestQuery(config, auth, className, where);
return query.execute().then((results) => {
let where = {'objectId': {'$in': Array.from(pointersHash[className])}};
var query = new RestQuery(config, auth, className, where, includeRestOptions);
return query.execute({op: 'get'}).then((results) => {
results.className = className;
return Promise.resolve(results);
})
@@ -541,7 +558,7 @@ function findPointers(object, path) {
return answer;
}
if (typeof object !== 'object') {
if (typeof object !== 'object' || !object) {
return [];
}
@@ -571,7 +588,7 @@ function replacePointers(object, path, replace) {
.filter((obj) => typeof obj !== 'undefined');
}
if (typeof object !== 'object') {
if (typeof object !== 'object' || !object) {
return object;
}
+23 -15
View File
@@ -561,31 +561,34 @@ RestWrite.prototype.handleInstallation = function() {
return;
}
if (!this.query && !this.data.deviceToken && !this.data.installationId) {
if (!this.query && !this.data.deviceToken && !this.data.installationId && !this.auth.installationId) {
throw new Parse.Error(135,
'at least one ID field (deviceToken, installationId) ' +
'must be specified in this operation');
}
if (!this.query && !this.data.deviceType) {
throw new Parse.Error(135,
'deviceType must be specified in this operation');
}
// If the device token is 64 characters long, we assume it is for iOS
// and lowercase it.
if (this.data.deviceToken && this.data.deviceToken.length == 64) {
this.data.deviceToken = this.data.deviceToken.toLowerCase();
}
// TODO: We may need installationId from headers, plumb through Auth?
// per installation_handler.go
// We lowercase the installationId if present
if (this.data.installationId) {
this.data.installationId = this.data.installationId.toLowerCase();
}
let installationId = this.data.installationId;
// If data.installationId is not set and we're not master, we can lookup in auth
if (!installationId && !this.auth.isMaster) {
installationId = this.auth.installationId;
}
if (installationId) {
installationId = installationId.toLowerCase();
}
var promise = Promise.resolve();
var idMatch; // Will be a match on either objectId or installationId
@@ -600,9 +603,9 @@ RestWrite.prototype.handleInstallation = function() {
objectId: this.query.objectId
});
}
if (this.data.installationId) {
if (installationId) {
orQueries.push({
'installationId': this.data.installationId
'installationId': installationId
});
}
if (this.data.deviceToken) {
@@ -622,7 +625,7 @@ RestWrite.prototype.handleInstallation = function() {
if (this.query && this.query.objectId && result.objectId == this.query.objectId) {
objectIdMatch = result;
}
if (result.installationId == this.data.installationId) {
if (result.installationId == installationId) {
installationIdMatch = result;
}
if (result.deviceToken == this.data.deviceToken) {
@@ -661,16 +664,21 @@ RestWrite.prototype.handleInstallation = function() {
idMatch = objectIdMatch;
}
if (this.data.installationId && installationIdMatch) {
if (installationId && installationIdMatch) {
idMatch = installationIdMatch;
}
// need to specify deviceType only if it's new
if (!this.query && !this.data.deviceType && !idMatch) {
throw new Parse.Error(135,
'deviceType must be specified in this operation');
}
}).then(() => {
if (!idMatch) {
if (!deviceTokenMatches.length) {
return;
} else if (deviceTokenMatches.length == 1 &&
(!deviceTokenMatches[0]['installationId'] || !this.data.installationId)
(!deviceTokenMatches[0]['installationId'] || !installationId)
) {
// Single match on device token but none on installationId, and either
// the passed object or the match is missing an installationId, so we
@@ -689,7 +697,7 @@ RestWrite.prototype.handleInstallation = function() {
var delQuery = {
'deviceToken': this.data.deviceToken,
'installationId': {
'$ne': this.data.installationId
'$ne': installationId
}
};
if (this.data.appIdentifier) {
+2 -2
View File
@@ -1,7 +1,7 @@
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
import _ from 'lodash';
import url from 'url';
const ALLOWED_GET_QUERY_KEYS = ['keys', 'include'];
@@ -115,7 +115,7 @@ export class ClassesRouter extends PromiseRouter {
static JSONFromQuery(query) {
let json = {};
for (let [key, value] of Object.entries(query)) {
for (let [key, value] of _.entries(query)) {
try {
json[key] = JSON.parse(value);
} catch (e) {
+1 -1
View File
@@ -103,7 +103,7 @@ export class FunctionsRouter extends PromiseRouter {
const applicationId = req.config.applicationId;
const theFunction = triggers.getFunction(functionName, applicationId);
const theValidator = triggers.getValidator(req.params.functionName, applicationId);
if (theFunction) {
if (theFunction) {
let params = Object.assign({}, req.body, req.query);
params = parseParams(params);
var request = {
+31 -5
View File
@@ -2,11 +2,10 @@
var https = require('https');
var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request("tokeninfo?id_token="+authData.access_token)
function validateIdToken(id, token) {
return request("tokeninfo?id_token="+token)
.then((response) => {
if (response && response.sub == authData.id) {
if (response && response.sub == id) {
return;
}
throw new Parse.Error(
@@ -15,7 +14,34 @@ function validateAuthData(authData) {
});
}
// Returns a promise that fulfills iff this app id is valid.
function validateAuthToken(id, token) {
return request("tokeninfo?access_token="+token)
.then((response) => {
if (response && response.user_id == id) {
return;
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Google auth is invalid for this user.');
});
}
// Returns a promise that fulfills if this user id is valid.
function validateAuthData(authData) {
if (authData.id_token) {
return validateIdToken(authData.id, authData.id_token);
} else {
return validateAuthToken(authData.id, authData.access_token).then(() => {
// Validation with auth token worked
return;
}, () => {
// Try with the id_token param
return validateIdToken(authData.id, authData.access_token);
});
}
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
}
+27 -8
View File
@@ -1,28 +1,47 @@
'use strict';
// Helper functions for accessing the vkontakte API.
var https = require('https');
var Parse = require('parse/node').Parse;
var logger = require('../logger').default;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request("users.get?v=V&access_token=" + authData.access_token).then(function (response) {
if (response && response.response && response.response[0].uid == authData.id) {
return;
function validateAuthData(authData, params) {
return vkOAuth2Request(params).then(function (response) {
if (response && response && response.access_token) {
return request("api.vk.com", "method/secure.checkToken?token=" + authData.access_token + "&client_secret=" + params.appSecret + "&access_token=" + response.access_token).then(function (response) {
if (response && response.response && response.response.user_id == authData.id) {
return;
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
});
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
logger.error('Vk Auth', 'Vk appIds or appSecret is incorrect.');
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk appIds or appSecret is incorrect.');
});
}
function vkOAuth2Request(params) {
var promise = new Parse.Promise();
return promise.then(function(){
if (!params || !params.appIds || !params.appIds.length || !params.appSecret || !params.appSecret.length ) {
logger.error('Vk Auth', 'Vk auth is not configured. Missing appIds or appSecret.');
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is not configured. Missing appIds or appSecret.');
}
return request("oauth.vk.com", "access_token?client_id=" + params.appIds + "&client_secret=" + params.appSecret + "&v=5.59&grant_type=client_credentials")
})
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for api requests
function request(path) {
function request(host, path) {
return new Promise(function (resolve, reject) {
https.get("https://api.vk.com/method/" + path, function (res) {
https.get("https://" + host + "/" + path, function (res) {
var data = '';
res.on('data', function (chunk) {
data += chunk;
@@ -40,4 +59,4 @@ function request(path) {
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData
};
};
@@ -0,0 +1,48 @@
import {
numberParser,
numberOrBoolParser,
objectParser,
arrayParser,
moduleOrObjectParser,
booleanParser,
nullParser
} from '../utils/parsers';
export default {
"appId": {
required: true,
help: "Required. This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId."
},
"masterKey": {
required: true,
help: "Required. This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey."
},
"serverURL": {
required: true,
help: "Required. This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL."
},
"redisURL": {
help: "Optional. This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey."
},
"keyPairs": {
help: "Optional. A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details."
},
"websocketTimeout": {
help: "Optional. Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients. Defaults to 10 * 1000 ms (10 s).",
action: numberParser("websocketTimeout")
},
"cacheTimeout": {
help: "Optional. Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details. Defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).",
action: numberParser("cacheTimeout")
},
"logLevel": {
help: "Optional. This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE. Defaults to INFO.",
},
"port": {
env: "PORT",
help: "The port to run the ParseServer. defaults to 1337.",
default: 1337,
action: numberParser("port")
},
};
@@ -1,52 +1,13 @@
function numberParser(key) {
return function(opt) {
opt = parseInt(opt);
if (!Number.isInteger(opt)) {
throw new Error(`The ${key} is invalid`);
}
return opt;
}
}
import {
numberParser,
numberOrBoolParser,
objectParser,
arrayParser,
moduleOrObjectParser,
booleanParser,
nullParser
} from '../utils/parsers';
function numberOrBoolParser(key) {
return function(opt) {
if (typeof opt === 'boolean') {
return opt;
}
return numberParser(key)(opt);
}
}
function objectParser(opt) {
if (typeof opt == 'object') {
return opt;
}
return JSON.parse(opt)
}
function moduleOrObjectParser(opt) {
if (typeof opt == 'object') {
return opt;
}
try {
return JSON.parse(opt);
} catch(e) {}
return opt;
}
function booleanParser(opt) {
if (opt == true || opt == "true" || opt == "1") {
return true;
}
return false;
}
function nullParser(opt) {
if (opt == 'null') {
return null;
}
return opt;
}
export default {
"appId": {
@@ -128,9 +89,7 @@ export default {
env: "PARSE_SERVER_FACEBOOK_APP_IDS",
help: "Comma separated list for your facebook app Ids",
type: "list",
action: function(opt) {
return opt.split(",")
}
action: arrayParser
},
"enableAnonymousUsers": {
env: "PARSE_SERVER_ENABLE_ANON_USERS",
@@ -239,5 +198,28 @@ export default {
"cluster": {
help: "Run with cluster, optionally set the number of processes default to os.cpus().length",
action: numberOrBoolParser("cluster")
}
},
"liveQuery": {
help: "parse-server's LiveQuery configuration object",
action: objectParser
},
"liveQuery.classNames": {
help: "parse-server's LiveQuery classNames",
action: arrayParser
},
"liveQuery.redisURL": {
help: "parse-server's LiveQuery redisURL",
},
"startLiveQueryServer": {
help: "Starts the liveQuery server",
action: booleanParser
},
"liveQueryPort": {
help: 'Specific port to start the live query server',
action: numberParser("liveQueryPort")
},
"liveQueryServerOptions": {
help: "Live query server configuration options (will start the liveQuery server)",
action: objectParser
},
};
+15
View File
@@ -0,0 +1,15 @@
import definitions from './definitions/parse-live-query-server';
import runner from './utils/runner';
import { ParseServer } from '../index';
import express from 'express';
runner({
definitions,
start: function(program, options, logOptions) {
logOptions();
var app = express();
var httpServer = require('http').createServer(app);
httpServer.listen(options.port);
ParseServer.createLiveQueryServer(httpServer, options);
}
})
+66 -58
View File
@@ -1,18 +1,12 @@
import path from 'path';
import express from 'express';
import { ParseServer } from '../index';
import definitions from './cli-definitions';
import program from './utils/commander';
import { mergeWithOptions } from './utils/commander';
import definitions from './definitions/parse-server';
import cluster from 'cluster';
import os from 'os';
import runner from './utils/runner';
program.loadDefinitions(definitions);
program
.usage('[options] <path/to/configuration.json>');
program.on('--help', function(){
const help = function(){
console.log(' Get Started guide:');
console.log('');
console.log(' Please have a look at the get started guide!')
@@ -32,33 +26,7 @@ program.on('--help', function(){
console.log(' $ parse-server -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL');
console.log(' $ parse-server -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL');
console.log('');
});
program.parse(process.argv, process.env);
let options = program.getOptions();
if (!options.serverURL) {
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
}
if (!options.appId || !options.masterKey || !options.serverURL) {
program.outputHelp();
console.error("");
console.error('\u001b[31mERROR: appId and masterKey are required\u001b[0m');
console.error("");
process.exit(1);
}
function logStartupOptions(options) {
for (let key in options) {
let value = options[key];
if (key == "masterKey") {
value = "***REDACTED***";
}
console.log(`${key}: ${value}`);
}
}
};
function startServer(options, callback) {
const app = express();
@@ -66,7 +34,15 @@ function startServer(options, callback) {
app.use(options.mountPath, api);
var server = app.listen(options.port, callback);
if (options.startLiveQueryServer || options.liveQueryServerOptions) {
let liveQueryServer = server;
if (options.liveQueryPort) {
liveQueryServer = express().listen(options.liveQueryPort, () => {
console.log('ParseLiveQuery listening on ' + options.liveQueryPort);
});
}
ParseServer.createLiveQueryServer(liveQueryServer, options.liveQueryServerOptions);
}
var handleShutdown = function() {
console.log('Termination signal received. Shutting down.');
server.close(function () {
@@ -77,27 +53,59 @@ function startServer(options, callback) {
process.on('SIGINT', handleShutdown);
}
if (options.cluster) {
const numCPUs = typeof options.cluster === 'number' ? options.cluster : os.cpus().length;
if (cluster.isMaster) {
logStartupOptions(options);
for(var i = 0; i < numCPUs; i++) {
cluster.fork();
runner({
definitions,
help,
usage: '[options] <path/to/configuration.json>',
start: function(program, options, logOptions) {
if (!options.serverURL) {
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died... Restarting`);
cluster.fork();
});
} else {
startServer(options, () => {
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
});
if (!options.appId || !options.masterKey || !options.serverURL) {
program.outputHelp();
console.error("");
console.error('\u001b[31mERROR: appId and masterKey are required\u001b[0m');
console.error("");
process.exit(1);
}
if (options["liveQuery.classNames"]) {
options.liveQuery = options.liveQuery || {};
options.liveQuery.classNames = options["liveQuery.classNames"];
delete options["liveQuery.classNames"];
}
if (options["liveQuery.redisURL"]) {
options.liveQuery = options.liveQuery || {};
options.liveQuery.redisURL = options["liveQuery.redisURL"];
delete options["liveQuery.redisURL"];
}
if (options.cluster) {
const numCPUs = typeof options.cluster === 'number' ? options.cluster : os.cpus().length;
if (cluster.isMaster) {
for(var i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died... Restarting`);
cluster.fork();
});
} else {
startServer(options, () => {
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
});
}
} else {
startServer(options, () => {
logOptions();
console.log('');
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
});
}
}
} else {
startServer(options, () => {
logStartupOptions(options);
console.log('');
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
});
}
})
+59
View File
@@ -0,0 +1,59 @@
export function numberParser(key) {
return function(opt) {
opt = parseInt(opt);
if (!Number.isInteger(opt)) {
throw new Error(`The ${key} is invalid`);
}
return opt;
}
}
export function numberOrBoolParser(key) {
return function(opt) {
if (typeof opt === 'boolean') {
return opt;
}
return numberParser(key)(opt);
}
}
export function objectParser(opt) {
if (typeof opt == 'object') {
return opt;
}
return JSON.parse(opt)
}
export function arrayParser(opt) {
if (Array.isArray(opt)) {
return opt;
} else if (typeof opt === 'string') {
return opt.split(',');
} else {
throw new Error(`${opt} should be a comma separated string or an array`);
}
}
export function moduleOrObjectParser(opt) {
if (typeof opt == 'object') {
return opt;
}
try {
return JSON.parse(opt);
} catch(e) {}
return opt;
}
export function booleanParser(opt) {
if (opt == true || opt == "true" || opt == "1") {
return true;
}
return false;
}
export function nullParser(opt) {
if (opt == 'null') {
return null;
}
return opt;
}
+37
View File
@@ -0,0 +1,37 @@
import program from './commander';
import { mergeWithOptions } from './commander';
function logStartupOptions(options) {
for (let key in options) {
let value = options[key];
if (key == "masterKey") {
value = "***REDACTED***";
}
if (typeof value === 'object') {
value = JSON.stringify(value);
}
console.log(`${key}: ${value}`);
}
}
export default function({
definitions,
help,
usage,
start
}) {
program.loadDefinitions(definitions);
if (usage) {
program.usage(usage);
}
if (help) {
program.on('--help', help);
}
program.parse(process.argv, process.env);
let options = program.getOptions();
start(program, options, function() {
logStartupOptions(options);
});
}
+14 -1
View File
@@ -1,5 +1,18 @@
'use strict';
let logger;
import defaults from './defaults';
import { WinstonLoggerAdapter } from './Adapters/Logger/WinstonLoggerAdapter';
import { LoggerController } from './Controllers/LoggerController';
function defaultLogger() {
let adapter = new WinstonLoggerAdapter({
logsFolder: defaults.logsFolder,
jsonLogs: defaults.jsonLogs,
verbose: defaults.verbose,
silent: defaults.silent });
return new LoggerController(adapter);
}
let logger = defaultLogger();
export function setLogger(aLogger) {
logger = aLogger;