Merge branch 'master' of https://github.com/ParsePlatform/parse-server into mcdonald-gcs-adapter
Get GCSAdapter up to snuff with FilesController + FilesControllerTestFactory * 'master' of https://github.com/ParsePlatform/parse-server: (102 commits) Remove duplicated instructions Release and Changelog for 2.1.4 fixes missing coverage with sh script Fix update system schema Adds optional COVERAGE Allows to pass no where in $select clause Sanitize objectId in Fix delete schema when actual collection does not exist Fix replace query overwrite the existing query object. Fix create system class with relation/pointer Use throws syntax for errors in SchemasRouter. Completely migrate SchemasRouter to new MongoCollection API. Add tests that verify installationId in Cloud Code triggers. Propagate installationId in all Cloud Code triggers. Add test expiresAt should be a Date, not a string. Fixes #776 Fix missing 'let/var' in OneSignalPushAdapter.spec. Don't run any afterSave hooks if none are registered. Fix : remove query count limit Flatten custom operations in request.object in afterSave hooks. ...
This commit is contained in:
@@ -5,8 +5,11 @@ language: node_js
|
||||
node_js:
|
||||
- "4.3"
|
||||
env:
|
||||
- MONGODB_VERSION=2.6.11
|
||||
- MONGODB_VERSION=3.0.8
|
||||
global:
|
||||
- CODE_COVERAGE=1
|
||||
matrix:
|
||||
- MONGODB_VERSION=2.6.11
|
||||
- MONGODB_VERSION=3.0.8
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.mongodb/versions/downloads
|
||||
|
||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,5 +1,41 @@
|
||||
## Parse Server Changelog
|
||||
|
||||
### 2.1.4 (3/3/2016)
|
||||
|
||||
* New: serverInfo endpoint that returns server version and info about the server's features
|
||||
* Improvement: Add support for badges on iOS
|
||||
* Improvement: Improve failure handling in cloud code http requests
|
||||
* Improvement: Add support for queries on pointers and relations
|
||||
* Improvement: Add support for multiple $in clauses in a query
|
||||
* Improvement: Add allowClientClassCreation config option
|
||||
* Improvement: Allow atomically setting subdocument keys
|
||||
* Improvement: Allow arbitrarily deeply nested roles
|
||||
* Improvement: Set proper content-type in S3 File Adapter
|
||||
* Improvement: S3 adapter auto-creates buckets
|
||||
* Improvement: Better error messages for many errors
|
||||
* Performance: Improved algorithm for validating client keys
|
||||
* Experimental: Parse Hooks and Hooks API
|
||||
* Experimental: Email verification and password reset emails
|
||||
* Experimental: Improve compatability of logs feature with Parse.com
|
||||
* Fix: Fix for attempting to delete missing classes via schemas API
|
||||
* Fix: Allow creation of system classes via schemas API
|
||||
* Fix: Allow missing where cause in $select
|
||||
* Fix: Improve handling of invalid object ids
|
||||
* Fix: Replace query overwriting existing query
|
||||
* Fix: Propagate installationId in cloud code triggers
|
||||
* Fix: Session expiresAt is now a Date instead of a string
|
||||
* Fix: Fix count queries
|
||||
* Fix: Disallow _Role objects without names or without ACL
|
||||
* Fix: Better handling of invalid types submitted
|
||||
* Fix: beforeSave will not be triggered for attempts to save with invalid authData
|
||||
* Fix: Fix duplicate device token issues on Android
|
||||
* Fix: Allow empty authData on signup
|
||||
* Fix: Allow Master Key Headers (CORS)
|
||||
* Fix: Fix bugs if JavaScript key was not provided in server configuration
|
||||
* Fix: Parse Files on objects can now be stored without URLs
|
||||
* Fix: allow both objectId or installationId when modifying installation
|
||||
* Fix: Command line works better when not given options
|
||||
|
||||
### 2.1.3 (2/24/2016)
|
||||
|
||||
* Feature: Add initial support for in-app purchases
|
||||
@@ -8,7 +44,7 @@
|
||||
* Performance: Faster saves if not using beforeSave triggers
|
||||
* Fix: Send session token in response to current user endpoint
|
||||
* Fix: Remove triggers for _Session collection
|
||||
* Fix: Improve compatability of Cloud Code beforeSave hook for newly created object
|
||||
* Fix: Improve compatability of cloud code beforeSave hook for newly created object
|
||||
* Fix: ACL creation for master key only objects
|
||||
* Fix: Allow uploading files without Content-Type
|
||||
* Fix: Add features to http requrest to match Parse.com
|
||||
@@ -41,7 +77,7 @@
|
||||
* Feature: Support for logs, extensible via Log Adapter
|
||||
* Feature: New Push Adapter for sending push notifications through OneSignal
|
||||
* Feature: Tighter default security for Users
|
||||
* Feature: Pass parameters to Cloud Code in query string
|
||||
* Feature: Pass parameters to cloud code in query string
|
||||
* Feature: Disable anonymous users via configuration.
|
||||
* Experimental: Schemas API support for PUT operations
|
||||
* Fix: Prevent installation ID from being added to User
|
||||
@@ -58,7 +94,7 @@
|
||||
### 2.0.8 (2/11/2016)
|
||||
|
||||
* Add: support for Android and iOS push notifications
|
||||
* Experimental: Cloud Code validation hooks (can mark as non-experimental after we have docs)
|
||||
* Experimental: cloud code validation hooks (can mark as non-experimental after we have docs)
|
||||
* Experimental: support for schemas API (GET and POST only)
|
||||
* Experimental: support for Parse Config (GET and POST only)
|
||||
* Fix: Querying objects with equality constraint on array column
|
||||
|
||||
21
README.md
21
README.md
@@ -44,10 +44,10 @@ app.listen(1337, function() {
|
||||
|
||||
### Standalone Parse Server
|
||||
|
||||
Parse Server can also run as a standalone API server.
|
||||
Parse Server can also run as a standalone API server.
|
||||
You can configure Parse Server with a configuration file, arguments and environment variables.
|
||||
|
||||
To start the server:
|
||||
To start the server:
|
||||
|
||||
`npm start -- --appId MYAPP --masterKey MASTER_KEY --serverURL http://localhost:1337/parse`.
|
||||
|
||||
@@ -65,22 +65,6 @@ The default port is 1337, to use a different port set the PORT environment varia
|
||||
|
||||
The standalone Parse Server can be configured using [environment variables](#configuration).
|
||||
|
||||
Please refer to the [configuration section](#configuration) or help;
|
||||
|
||||
To get more help for running the parse-server standalone, you can run:
|
||||
|
||||
`$ npm start -- --help`
|
||||
|
||||
The standalone API server supports loading a configuration file in JSON format:
|
||||
|
||||
`$ npm start -- path/to/your/config.json`
|
||||
|
||||
The default port is 1337, to use a different port set the `--port` option:
|
||||
|
||||
`$ npm start -- --port=8080 path/to/your/config.json`
|
||||
|
||||
Please refer to the [configuration section](#configuration) or help;
|
||||
|
||||
You can also install Parse Server globally:
|
||||
|
||||
`$ npm install -g parse-server`
|
||||
@@ -126,6 +110,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
* `databaseAdapter` (unfinished) - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`)
|
||||
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js))
|
||||
* `enableAnonymousUsers` - Defaults to true. Set to false to disable anonymous users.
|
||||
* `allowClientClassCreation` - Defaults to true. Set to false to disable client class creation.
|
||||
* `oauth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth).
|
||||
* `maxUploadSize` - Defaults to 20mb. Max file size for uploads
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "2.1.3",
|
||||
"version": "2.1.4",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -27,6 +27,7 @@
|
||||
"deepcopy": "^0.6.1",
|
||||
"express": "^4.13.4",
|
||||
"gcloud": "^0.28.0",
|
||||
"mailgun-js": "^0.7.7",
|
||||
"mime": "^1.3.4",
|
||||
"mongodb": "~2.1.0",
|
||||
"multer": "^1.1.0",
|
||||
@@ -56,7 +57,7 @@
|
||||
"dev": "npm run build && bin/dev",
|
||||
"build": "./node_modules/.bin/babel src/ -d lib/",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start",
|
||||
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js",
|
||||
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $(if [ \"$CODE_COVERAGE\" = \"1\" ]; then echo './node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'; fi;) ./node_modules/jasmine/bin/jasmine.js",
|
||||
"posttest": "mongodb-runner stop",
|
||||
"start": "./bin/parse-server",
|
||||
"prepublish": "npm run build"
|
||||
|
||||
43
public_html/invalid_link.html
Normal file
43
public_html/invalid_link.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- This page is displayed when someone navigates to a verify email or reset password link
|
||||
but their security token is wrong. This can either mean the user has clicked on a
|
||||
stale link (i.e. re-click on a password reset link after resetting their password) or
|
||||
(rarely) this could be a sign of a malicious user trying to tamper with your app.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Invalid Link</title>
|
||||
<style type='text/css'>
|
||||
.container {
|
||||
border-width: 0px;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Helvetica Neue', Helvetica;
|
||||
font-size: 16px;
|
||||
height: 30px;
|
||||
line-height: 16px;
|
||||
margin: 45px 0px 0px 45px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
color: #0067AB;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 0 0 15px 0;
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Invalid Link</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
27
public_html/password_reset_success.html
Normal file
27
public_html/password_reset_success.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!-- This page is displayed whenever someone has successfully reset their password.
|
||||
Pro and Enterprise accounts may edit this page and tell Parse to use that custom
|
||||
version in their Parse app. See the App Settigns page for more information.
|
||||
This page will be called with the query param 'username'
|
||||
-->
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
<style type='text/css'>
|
||||
h1 {
|
||||
color: #0067AB;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 45px 0px 0px 45px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Successfully updated your password!</h1>
|
||||
</body>
|
||||
</html>
|
||||
27
public_html/verify_email_success.html
Normal file
27
public_html/verify_email_success.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!-- This page is displayed whenever someone has successfully reset their password.
|
||||
Pro and Enterprise accounts may edit this page and tell Parse to use that custom
|
||||
version in their Parse app. See the App Settigns page for more information.
|
||||
This page will be called with the query param 'username'
|
||||
-->
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
<style type='text/css'>
|
||||
h1 {
|
||||
color: #0067AB;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 45px 0px 0px 45px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Successfully verified your email!</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,15 +2,17 @@
|
||||
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
|
||||
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
|
||||
|
||||
describe("AdaptableController", ()=>{
|
||||
describe("AdapterLoader", ()=>{
|
||||
|
||||
it("should instantiate an adapter from string in object", (done) => {
|
||||
var adapterPath = require('path').resolve("./spec/MockAdapter");
|
||||
|
||||
var adapter = loadAdapter({
|
||||
adapter: adapterPath,
|
||||
key: "value",
|
||||
foo: "bar"
|
||||
options: {
|
||||
key: "value",
|
||||
foo: "bar"
|
||||
}
|
||||
});
|
||||
|
||||
expect(adapter instanceof Object).toBe(true);
|
||||
@@ -24,7 +26,6 @@ describe("AdaptableController", ()=>{
|
||||
var adapter = loadAdapter(adapterPath);
|
||||
|
||||
expect(adapter instanceof Object).toBe(true);
|
||||
expect(adapter.options).toBe(adapterPath);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -65,4 +66,22 @@ describe("AdaptableController", ()=>{
|
||||
expect(adapter).toBe(originalAdapter);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail loading an improperly configured adapter", (done) => {
|
||||
var Adapter = function(options) {
|
||||
if (!options.foo) {
|
||||
throw "foo is required for that adapter";
|
||||
}
|
||||
}
|
||||
var adapterOptions = {
|
||||
param: "key",
|
||||
doSomething: function() {}
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
var adapter = loadAdapter(adapterOptions, Adapter);
|
||||
expect(adapter).toEqual(adapterOptions);
|
||||
}).not.toThrow("foo is required for that adapter");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
17
spec/DatabaseController.spec.js
Normal file
17
spec/DatabaseController.spec.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
let DatabaseController = require('../src/Controllers/DatabaseController');
|
||||
let MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
||||
|
||||
describe('DatabaseController', () => {
|
||||
it('can be constructed', done => {
|
||||
let adapter = new MongoStorageAdapter('mongodb://localhost:27017/test');
|
||||
let databaseController = new DatabaseController(adapter, {
|
||||
collectionPrefix: 'test_'
|
||||
});
|
||||
databaseController.connect().then(done, error => {
|
||||
console.log('error', error.stack);
|
||||
fail();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
var ExportAdapter = require('../src/ExportAdapter');
|
||||
|
||||
describe('ExportAdapter', () => {
|
||||
it('can be constructed', (done) => {
|
||||
var database = new ExportAdapter('mongodb://localhost:27017/test',
|
||||
{
|
||||
collectionPrefix: 'test_'
|
||||
});
|
||||
database.connect().then(done, (error) => {
|
||||
console.log('error', error.stack);
|
||||
fail();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,29 +1,52 @@
|
||||
var FilesController = require('../src/Controllers/FilesController').FilesController;
|
||||
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
|
||||
var S3Adapter = require("../src/Adapters/Files/S3Adapter").S3Adapter;
|
||||
var GCSAdapter = require("../src/Adapters/Files/GCSAdapter").GCSAdapter;
|
||||
var Config = require("../src/Config");
|
||||
|
||||
var FCTestFactory = require("./FilesControllerTestFactory");
|
||||
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe("FilesController",()=>{
|
||||
|
||||
it("should properly expand objects", (done) => {
|
||||
var config = new Config(Parse.applicationId);
|
||||
var adapter = new GridStoreAdapter();
|
||||
var filesController = new FilesController(adapter);
|
||||
var result = filesController.expandFilesInObject(config, function(){});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
var fullFile = {
|
||||
type: '__type',
|
||||
url: "http://an.url"
|
||||
}
|
||||
|
||||
var anObject = {
|
||||
aFile: fullFile
|
||||
}
|
||||
filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual("http://an.url");
|
||||
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
// Test the grid store adapter
|
||||
var gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
|
||||
FCTestFactory.testAdapter("GridStoreAdapter", gridStoreAdapter);
|
||||
|
||||
if (process.env.S3_ACCESS_KEY && process.env.S3_SECRET_KEY) {
|
||||
|
||||
// Test the S3 Adapter
|
||||
var s3Adapter = new S3Adapter(process.env.S3_ACCESS_KEY, process.env.S3_SECRET_KEY, 'parse.server.tests');
|
||||
|
||||
FCTestFactory.testAdapter("S3Adapter",s3Adapter);
|
||||
|
||||
// Test S3 with direct access
|
||||
var s3DirectAccessAdapter = new S3Adapter(process.env.S3_ACCESS_KEY, process.env.S3_SECRET_KEY, 'parse.server.tests', {
|
||||
directAccess: true
|
||||
});
|
||||
|
||||
FCTestFactory.testAdapter("S3AdapterDirect", s3DirectAccessAdapter);
|
||||
|
||||
} else if (!process.env.TRAVIS) {
|
||||
console.log("set S3_ACCESS_KEY and S3_SECRET_KEY to test S3Adapter")
|
||||
}
|
||||
|
||||
if (process.env.GCP_PROJECT_ID && process.env.GCP_KEYFILE_PATH && process.env.GCS_BUCKET_NAME) {
|
||||
|
||||
// Test the GCS Adapter
|
||||
var gcsAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET_NAME);
|
||||
|
||||
FCTestFactory.testAdapter("GCSAdapter", gcsAdapter);
|
||||
|
||||
// Test GCS with direct access
|
||||
var gcsDirectAccessAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET_NAME, {
|
||||
directAccess: true
|
||||
});
|
||||
|
||||
FCTestFactory.testAdapter("GCSAdapterDirect", gcsDirectAccessAdapter);
|
||||
|
||||
} else if (!process.env.TRAVIS) {
|
||||
console.log("set GCP_PROJECT_ID, GCP_KEYFILE_PATH, and GCS_BUCKET_NAME to test GCSAdapter")
|
||||
}
|
||||
});
|
||||
|
||||
73
spec/FilesControllerTestFactory.js
Normal file
73
spec/FilesControllerTestFactory.js
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
var FilesController = require('../src/Controllers/FilesController').FilesController;
|
||||
var Config = require("../src/Config");
|
||||
|
||||
var testAdapter = function(name, adapter) {
|
||||
// Small additional tests to improve overall coverage
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var filesController = new FilesController(adapter);
|
||||
|
||||
describe("FilesController with "+name,()=>{
|
||||
|
||||
it("should properly expand objects", (done) => {
|
||||
|
||||
var result = filesController.expandFilesInObject(config, function(){});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
var fullFile = {
|
||||
type: '__type',
|
||||
url: "http://an.url"
|
||||
}
|
||||
|
||||
var anObject = {
|
||||
aFile: fullFile
|
||||
}
|
||||
filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual("http://an.url");
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
it("should properly create, read, delete files", (done) => {
|
||||
var filename;
|
||||
filesController.createFile(config, "file.txt", "hello world").then( (result) => {
|
||||
ok(result.url);
|
||||
ok(result.name);
|
||||
filename = result.name;
|
||||
expect(result.name.match(/file.txt/)).not.toBe(null);
|
||||
return filesController.getFileData(config, filename);
|
||||
}, (err) => {
|
||||
fail("The adapter should create the file");
|
||||
console.error(err);
|
||||
done();
|
||||
}).then((result) => {
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual("hello world");
|
||||
return filesController.deleteFile(config, filename);
|
||||
}, (err) => {
|
||||
fail("The adapter should get the file");
|
||||
console.error(err);
|
||||
done();
|
||||
}).then((result) => {
|
||||
|
||||
filesController.getFileData(config, filename).then((res) => {
|
||||
fail("the file should be deleted");
|
||||
done();
|
||||
}, (err) => {
|
||||
done();
|
||||
});
|
||||
|
||||
}, (err) => {
|
||||
fail("The adapter should delete the file");
|
||||
console.error(err);
|
||||
done();
|
||||
});
|
||||
}, 5000); // longer tests
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testAdapter: testAdapter
|
||||
}
|
||||
@@ -177,5 +177,20 @@ describe("httpRequest", () => {
|
||||
var result = httpRequest.encodeBody({"foo": "bar", "bar": "baz"}, {'X-Custom-Header': 'my-header'});
|
||||
expect(result).toEqual({"foo": "bar", "bar": "baz"});
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail gracefully", (done) => {
|
||||
httpRequest({
|
||||
url: "http://not a good url",
|
||||
success: function() {
|
||||
fail("should not succeed");
|
||||
done();
|
||||
},
|
||||
error: function(error) {
|
||||
expect(error).not.toBeUndefined();
|
||||
expect(error).not.toBeNull();
|
||||
done();
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const request = require('request');
|
||||
var LogsRouter = require('../src/Routers/LogsRouter').LogsRouter;
|
||||
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
|
||||
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
|
||||
@@ -20,7 +23,7 @@ describe('LogsRouter', () => {
|
||||
var router = new LogsRouter();
|
||||
|
||||
expect(() => {
|
||||
router.handleGET(request);
|
||||
router.validateRequest(request);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
@@ -40,28 +43,23 @@ describe('LogsRouter', () => {
|
||||
var router = new LogsRouter();
|
||||
|
||||
expect(() => {
|
||||
router.handleGET(request);
|
||||
router.validateRequest(request);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can check invalid master key of request', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
auth: {
|
||||
isMaster: false
|
||||
},
|
||||
query: {},
|
||||
config: {
|
||||
loggerController: loggerController
|
||||
it('can check invalid master key of request', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/scriptlog',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
}
|
||||
};
|
||||
|
||||
var router = new LogsRouter();
|
||||
|
||||
expect(() => {
|
||||
router.handleGET(request);
|
||||
}).toThrow();
|
||||
done();
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
module.exports = function(options) {
|
||||
this.options = options;
|
||||
}
|
||||
return {
|
||||
options: options
|
||||
};
|
||||
};
|
||||
|
||||
5
spec/MockEmailAdapter.js
Normal file
5
spec/MockEmailAdapter.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
10
spec/MockEmailAdapterWithOptions.js
Normal file
10
spec/MockEmailAdapterWithOptions.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = options => {
|
||||
if (!options) {
|
||||
throw "Options were not provided"
|
||||
}
|
||||
return {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter');
|
||||
var classifyInstallations = require('../src/Adapters/Push/PushAdapterUtils').classifyInstallations;
|
||||
|
||||
// Make mock config
|
||||
var pushConfig = {
|
||||
oneSignalAppId:"APP ID",
|
||||
oneSignalApiKey:"API KEY"
|
||||
};
|
||||
|
||||
describe('OneSignalPushAdapter', () => {
|
||||
it('can be initialized', (done) => {
|
||||
// Make mock config
|
||||
var pushConfig = {
|
||||
oneSignalAppId:"APP ID",
|
||||
oneSignalApiKey:"API KEY"
|
||||
};
|
||||
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||
|
||||
@@ -17,9 +20,17 @@ describe('OneSignalPushAdapter', () => {
|
||||
expect(senderMap.android instanceof Function).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('cannot be initialized if options are missing', (done) => {
|
||||
|
||||
expect(() => {
|
||||
new OneSignalPushAdapter();
|
||||
}).toThrow("Trying to initialize OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey");
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get valid push types', (done) => {
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||
|
||||
expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
|
||||
done();
|
||||
@@ -56,7 +67,7 @@ describe('OneSignalPushAdapter', () => {
|
||||
|
||||
|
||||
it('can send push notifications', (done) => {
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||
|
||||
// Mock android ios senders
|
||||
var androidSender = jasmine.createSpy('send')
|
||||
@@ -108,7 +119,7 @@ describe('OneSignalPushAdapter', () => {
|
||||
});
|
||||
|
||||
it("can send iOS notifications", (done) => {
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
|
||||
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
|
||||
|
||||
@@ -135,7 +146,7 @@ describe('OneSignalPushAdapter', () => {
|
||||
});
|
||||
|
||||
it("can send Android notifications", (done) => {
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
|
||||
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
|
||||
|
||||
@@ -157,10 +168,7 @@ describe('OneSignalPushAdapter', () => {
|
||||
});
|
||||
|
||||
it("can post the correct data", (done) => {
|
||||
var pushConfig = {
|
||||
oneSignalAppId:"APP ID",
|
||||
oneSignalApiKey:"API KEY"
|
||||
};
|
||||
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||
|
||||
var write = jasmine.createSpy('write');
|
||||
@@ -203,7 +211,7 @@ describe('OneSignalPushAdapter', () => {
|
||||
expect(write).toHaveBeenCalled();
|
||||
|
||||
// iOS
|
||||
args = write.calls.first().args;
|
||||
let args = write.calls.first().args;
|
||||
expect(args[0]).toEqual(JSON.stringify({
|
||||
'contents': { 'en':'Example content'},
|
||||
'content_available':true,
|
||||
@@ -212,7 +220,7 @@ describe('OneSignalPushAdapter', () => {
|
||||
'app_id':'APP ID'
|
||||
}));
|
||||
|
||||
// Android
|
||||
// Android
|
||||
args = write.calls.mostRecent().args;
|
||||
expect(args[0]).toEqual(JSON.stringify({
|
||||
'contents': { 'en':'Example content'},
|
||||
|
||||
@@ -372,6 +372,15 @@ describe('miscellaneous', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function shoud echo keys', function(done) {
|
||||
Parse.Cloud.run('echoKeys').then((result) => {
|
||||
expect(result.applicationId).toEqual(Parse.applicationId);
|
||||
expect(result.masterKey).toEqual(Parse.masterKey);
|
||||
expect(result.javascriptKey).toEqual(Parse.javascriptKey);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test rest_create_app', function(done) {
|
||||
var appId;
|
||||
@@ -683,6 +692,46 @@ describe('miscellaneous', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('afterSave flattens custom operations', done => {
|
||||
var triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.afterSave('GameScore', function(req, res) {
|
||||
let object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
let originalObject = req.original;
|
||||
if (triggerTime == 0) {
|
||||
// Create
|
||||
expect(object.get('yolo')).toEqual(1);
|
||||
} else if (triggerTime == 1) {
|
||||
// Update
|
||||
expect(object.get('yolo')).toEqual(2);
|
||||
// Check the originalObject
|
||||
expect(originalObject.get('yolo')).toEqual(1);
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('GameScore');
|
||||
obj.increment('yolo', 1);
|
||||
obj.save().then(() => {
|
||||
obj.increment('yolo', 1);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
// Clear mock afterSave
|
||||
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
|
||||
done();
|
||||
}, error => {
|
||||
console.error(error);
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function error handling', (done) => {
|
||||
// Register a function which will fail
|
||||
Parse.Cloud.define('willFail', (req, res) => {
|
||||
@@ -700,6 +749,80 @@ describe('miscellaneous', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeSave/afterSave get installationId', function(done) {
|
||||
let triggerTime = 0;
|
||||
Parse.Cloud.beforeSave('GameScore', function(req, res) {
|
||||
triggerTime++;
|
||||
expect(triggerTime).toEqual(1);
|
||||
expect(req.installationId).toEqual('yolo');
|
||||
res.success();
|
||||
});
|
||||
Parse.Cloud.afterSave('GameScore', function(req) {
|
||||
triggerTime++;
|
||||
expect(triggerTime).toEqual(2);
|
||||
expect(req.installationId).toEqual('yolo');
|
||||
});
|
||||
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': 'yolo'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore',
|
||||
body: JSON.stringify({ a: 'b' })
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(triggerTime).toEqual(2);
|
||||
|
||||
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
|
||||
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeDelete/afterDelete get installationId', function(done) {
|
||||
let triggerTime = 0;
|
||||
Parse.Cloud.beforeDelete('GameScore', function(req, res) {
|
||||
triggerTime++;
|
||||
expect(triggerTime).toEqual(1);
|
||||
expect(req.installationId).toEqual('yolo');
|
||||
res.success();
|
||||
});
|
||||
Parse.Cloud.afterDelete('GameScore', function(req) {
|
||||
triggerTime++;
|
||||
expect(triggerTime).toEqual(2);
|
||||
expect(req.installationId).toEqual('yolo');
|
||||
});
|
||||
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': 'yolo'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore',
|
||||
body: JSON.stringify({ a: 'b' })
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
request.del({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore/' + JSON.parse(body).objectId
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(triggerTime).toEqual(2);
|
||||
|
||||
Parse.Cloud._removeHook("Triggers", "beforeDelete", "GameScore");
|
||||
Parse.Cloud._removeHook("Triggers", "afterDelete", "GameScore");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function query parameters', (done) => {
|
||||
Parse.Cloud.define('echoParams', (req, res) => {
|
||||
res.success(req.params);
|
||||
@@ -844,4 +967,32 @@ describe('miscellaneous', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('dedupes an installation properly and returns updatedAt', (done) => {
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
let data = {
|
||||
'installationId': 'lkjsahdfkjhsdfkjhsdfkjhsdf',
|
||||
'deviceType': 'embedded'
|
||||
};
|
||||
let requestOptions = {
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/installations',
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
request.post(requestOptions, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
let b = JSON.parse(body);
|
||||
expect(typeof b.objectId).toEqual('string');
|
||||
request.post(requestOptions, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
let b = JSON.parse(body);
|
||||
expect(typeof b.updatedAt).toEqual('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
// This is a port of the test suite:
|
||||
// hungry/js/test/parse_file_test.js
|
||||
|
||||
"use strict";
|
||||
|
||||
var request = require('request');
|
||||
var GCSAdapter = require('../src/index').GCSAdapter;
|
||||
|
||||
var str = "Hello World!";
|
||||
var data = [];
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
data.push(str.charCodeAt(i));
|
||||
}
|
||||
|
||||
// Make sure that you fill these in, otherwise the tests won't run!!!
|
||||
var GCP_PROJECT_ID = "<gcp_project_id>";
|
||||
var GCP_KEYFILE_PATH = "<path/to/keyfile>";
|
||||
var GCS_BUCKET_NAME = "<gcs_bucket_name>";
|
||||
|
||||
// Note the 'xdescribe', make sure to delete the 'x' once the above vars
|
||||
// are filled in to run the test suite
|
||||
xdescribe('Parse.File GCS testing', () => {
|
||||
describe('GCS directAccess: false', () => {
|
||||
beforeEach(function(done){
|
||||
var port = 8378;
|
||||
var GCSConfiguration = {
|
||||
databaseURI: process.env.DATABASE_URI,
|
||||
serverURL: 'http://localhost:' + port + '/1',
|
||||
appId: 'test',
|
||||
javascriptKey: 'test',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
fileKey: 'test',
|
||||
filesAdapter: new GCSAdapter(
|
||||
GCP_PROJECT_ID,
|
||||
GCP_KEYFILE_PATH,
|
||||
GCS_BUCKET_NAME,
|
||||
{
|
||||
bucketPrefix: 'private/',
|
||||
directAccess: false
|
||||
}
|
||||
)
|
||||
};
|
||||
setServerConfiguration(GCSConfiguration);
|
||||
done();
|
||||
});
|
||||
|
||||
it('works with Content-Type', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.txt',
|
||||
body: 'argle bargle',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.txt$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('argle bargle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works without Content-Type', done => {
|
||||
var headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.txt',
|
||||
body: 'argle bargle',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.txt$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('argle bargle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('supports REST end-to-end file create, read, delete, read', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/testfile.txt',
|
||||
body: 'check one two',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_testfile.txt$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*testfile.txt$/);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('check one two');
|
||||
request.del({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test'
|
||||
},
|
||||
url: 'http://localhost:8378/1/files/' + b.name
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(response.statusCode).toEqual(200);
|
||||
request.get({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
},
|
||||
url: b.url
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(response.statusCode).toEqual(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('blocks file deletions with missing or incorrect master-key header', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/thefile.jpg',
|
||||
body: 'the file body'
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/);
|
||||
// missing X-Parse-Master-Key header
|
||||
request.del({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
},
|
||||
url: 'http://localhost:8378/1/files/' + b.name
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var del_b = JSON.parse(body);
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(del_b.error).toMatch(/unauthorized/);
|
||||
// incorrect X-Parse-Master-Key header
|
||||
request.del({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'tryagain'
|
||||
},
|
||||
url: 'http://localhost:8378/1/files/' + b.name
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var del_b2 = JSON.parse(body);
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(del_b2.error).toMatch(/unauthorized/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles other filetypes', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.jpg',
|
||||
body: 'argle bargle',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.jpg$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/.*file.jpg$/);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('argle bargle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GCS directAccess: true', () => {
|
||||
beforeEach(function(done){
|
||||
var port = 8378;
|
||||
var GCSConfiguration = {
|
||||
databaseURI: process.env.DATABASE_URI,
|
||||
serverURL: 'http://localhost:' + port + '/1',
|
||||
appId: 'test',
|
||||
javascriptKey: 'test',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
fileKey: 'test',
|
||||
filesAdapter: new GCSAdapter(
|
||||
GCP_PROJECT_ID,
|
||||
GCP_KEYFILE_PATH,
|
||||
GCS_BUCKET_NAME,
|
||||
{
|
||||
bucketPrefix: 'public/',
|
||||
directAccess: true
|
||||
}
|
||||
)
|
||||
};
|
||||
setServerConfiguration(GCSConfiguration);
|
||||
done();
|
||||
});
|
||||
|
||||
it('works with Content-Type', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.txt',
|
||||
body: 'argle bargle',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.txt$/);
|
||||
var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*file.txt")
|
||||
expect(b.url).toMatch(gcsRegex);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('argle bargle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works without Content-Type', done => {
|
||||
var headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.txt',
|
||||
body: 'argle bargle',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.txt$/);
|
||||
var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*file.txt")
|
||||
expect(b.url).toMatch(gcsRegex);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('argle bargle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('supports REST end-to-end file create, read, delete, read', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
// Create the file
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/testfile.txt',
|
||||
body: 'check one two',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_testfile.txt$/);
|
||||
var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*testfile.txt")
|
||||
expect(b.url).toMatch(gcsRegex);
|
||||
// Read the file the first time
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('check one two');
|
||||
// Delete the file
|
||||
request.del({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test'
|
||||
},
|
||||
url: 'http://localhost:8378/1/files/' + b.name
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(response.statusCode).toEqual(200);
|
||||
// Read the file the second time--expect it to be gone
|
||||
// Note that we're reading from the public cloud storage URL
|
||||
// This is different from the above test since it's assumed
|
||||
// users are reading from the public URL
|
||||
request.get({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
},
|
||||
url: "https://" + GCS_BUCKET_NAME + ".storage.googleapis.com/public/.*testfile.txt"
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(response.statusCode).toEqual(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('blocks file deletions with missing or incorrect master-key header', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/thefile.jpg',
|
||||
body: 'the file body'
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*thefile.jpg")
|
||||
expect(b.url).toMatch(gcsRegex);
|
||||
// missing X-Parse-Master-Key header
|
||||
request.del({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
},
|
||||
url: 'http://localhost:8378/1/files/' + b.name
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var del_b = JSON.parse(body);
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(del_b.error).toMatch(/unauthorized/);
|
||||
// incorrect X-Parse-Master-Key header
|
||||
request.del({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'tryagain'
|
||||
},
|
||||
url: 'http://localhost:8378/1/files/' + b.name
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var del_b2 = JSON.parse(body);
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(del_b2.error).toMatch(/unauthorized/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles other filetypes', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.jpg',
|
||||
body: 'argle bargle',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.jpg$/);
|
||||
var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*file.jpg")
|
||||
expect(b.url).toMatch(gcsRegex);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('argle bargle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
var request = require('request');
|
||||
var Parse = require('parse/node').Parse;
|
||||
var DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||
|
||||
let database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
|
||||
let Config = require('../src/Config');
|
||||
|
||||
describe('a GlobalConfig', () => {
|
||||
beforeEach(function(done) {
|
||||
database.rawCollection('_GlobalConfig')
|
||||
let config = new Config('test');
|
||||
config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.updateOne({ '_id': 1}, { $set: { params: { companies: ['US', 'DK'] } } }, { upsert: true }))
|
||||
.then(done());
|
||||
});
|
||||
@@ -54,14 +53,15 @@ describe('a GlobalConfig', () => {
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(body.error).toEqual('unauthorized');
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('failed getting config when it is missing', (done) => {
|
||||
database.rawCollection('_GlobalConfig')
|
||||
let config = new Config('test');
|
||||
config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.deleteOne({ '_id': 1}, {}, {}))
|
||||
.then(_ => {
|
||||
request.get({
|
||||
|
||||
@@ -446,6 +446,52 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update android device token with duplicate device token', (done) => {
|
||||
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
var input = {
|
||||
'installationId': installId1,
|
||||
'deviceToken': t,
|
||||
'deviceType': 'android'
|
||||
};
|
||||
var firstObject;
|
||||
var secondObject;
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
input = {
|
||||
'installationId': installId2,
|
||||
'deviceType': 'android'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId1}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
firstObject = results[0];
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId2}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
secondObject = results[0];
|
||||
// Update second installation to conflict with first installation
|
||||
input = {
|
||||
'objectId': secondObject._id,
|
||||
'deviceToken': t
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
secondObject._id, input);
|
||||
}).then(() => {
|
||||
// The first object should have been deleted
|
||||
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(0);
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
|
||||
it('update ios device token with duplicate device token', (done) => {
|
||||
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"use strict";
|
||||
// This is a port of the test suite:
|
||||
// hungry/js/test/parse_object_test.js
|
||||
//
|
||||
@@ -336,6 +337,34 @@ describe('Parse.Object testing', () => {
|
||||
item.save({ "foo^bar": "baz" }).then(fail, done);
|
||||
});
|
||||
|
||||
it("invalid __type", function(done) {
|
||||
var item = new Parse.Object("Item");
|
||||
var types = ['Pointer', 'File', 'Date', 'GeoPoint', 'Bytes'];
|
||||
var Error = Parse.Error;
|
||||
var tests = types.map(type => {
|
||||
var test = new Parse.Object("Item");
|
||||
test.set('foo', {
|
||||
__type: type
|
||||
});
|
||||
return test;
|
||||
});
|
||||
var next = function(index) {
|
||||
if (index < tests.length) {
|
||||
tests[index].save().then(fail, error => {
|
||||
expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||
next(index + 1);
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
item.save({
|
||||
"foo": {
|
||||
__type: "IvalidName"
|
||||
}
|
||||
}).then(fail, err => next(0));
|
||||
});
|
||||
|
||||
it("simple field deletion", function(done) {
|
||||
var simple = new Parse.Object("SimpleObject");
|
||||
simple.save({
|
||||
@@ -1763,6 +1792,55 @@ describe('Parse.Object testing', () => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have undefined includes when object is missing', (done) => {
|
||||
let obj1 = new Parse.Object("AnObject");
|
||||
let obj2 = new Parse.Object("AnObject");
|
||||
|
||||
Parse.Object.saveAll([obj1, obj2]).then(() => {
|
||||
obj1.set("obj", obj2);
|
||||
// Save the pointer, delete the pointee
|
||||
return obj1.save().then(() => { return obj2.destroy() });
|
||||
}).then(() => {
|
||||
let query = new Parse.Query("AnObject");
|
||||
query.include("obj");
|
||||
return query.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].get("obj")).toBe(undefined);
|
||||
let query = new Parse.Query("AnObject");
|
||||
return query.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].get("obj")).not.toBe(undefined);
|
||||
return res[0].get("obj").fetch();
|
||||
}).then(() => {
|
||||
fail("Should not fetch a deleted object");
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||
done();
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('should have undefined includes when object is missing on deeper path', (done) => {
|
||||
let obj1 = new Parse.Object("AnObject");
|
||||
let obj2 = new Parse.Object("AnObject");
|
||||
let obj3 = new Parse.Object("AnObject");
|
||||
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
|
||||
obj1.set("obj", obj2);
|
||||
obj2.set("obj", obj3);
|
||||
// Save the pointer, delete the pointee
|
||||
return Parse.Object.saveAll([obj1, obj2]).then(() => { return obj3.destroy() });
|
||||
}).then(() => {
|
||||
let query = new Parse.Query("AnObject");
|
||||
query.include("obj.obj");
|
||||
return query.get(obj1.id);
|
||||
}).then((res) => {
|
||||
expect(res.get("obj")).not.toBe(undefined);
|
||||
expect(res.get("obj").get("obj")).toBe(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2088,4 +2088,60 @@ describe('Parse.Query testing', () => {
|
||||
console.log(error);
|
||||
});
|
||||
});
|
||||
|
||||
// #371
|
||||
it('should properly interpret a query', (done) => {
|
||||
var query = new Parse.Query("C1");
|
||||
var auxQuery = new Parse.Query("C1");
|
||||
query.matchesKeyInQuery("A1", "A2", auxQuery);
|
||||
query.include("A3");
|
||||
query.include("A2");
|
||||
query.find().then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not failt");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should properly interpret a query', (done) => {
|
||||
var user = new Parse.User();
|
||||
user.set("username", "foo");
|
||||
user.set("password", "bar");
|
||||
return user.save().then( (user) => {
|
||||
var objIdQuery = new Parse.Query("_User").equalTo("objectId", user.id);
|
||||
var blockedUserQuery = user.relation("blockedUsers").query();
|
||||
|
||||
var aResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
|
||||
aResponseQuery.equalTo("userA", user);
|
||||
aResponseQuery.equalTo("userAResponse", 1);
|
||||
|
||||
var bResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
|
||||
bResponseQuery.equalTo("userB", user);
|
||||
bResponseQuery.equalTo("userBResponse", 1);
|
||||
|
||||
var matchOr = Parse.Query.or(aResponseQuery, bResponseQuery);
|
||||
var matchRelationshipA = new Parse.Query("_User");
|
||||
matchRelationshipA.matchesKeyInQuery("objectId", "userAObjectId", matchOr);
|
||||
var matchRelationshipB = new Parse.Query("_User");
|
||||
matchRelationshipB.matchesKeyInQuery("objectId", "userBObjectId", matchOr);
|
||||
|
||||
|
||||
var orQuery = Parse.Query.or(objIdQuery, blockedUserQuery, matchRelationshipA, matchRelationshipB);
|
||||
var query = new Parse.Query("_User");
|
||||
query.doesNotMatchQuery("objectId", orQuery);
|
||||
return query.find();
|
||||
}).then((res) => {
|
||||
done();
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -237,7 +237,7 @@ describe('Parse.Relation testing', () => {
|
||||
success: function(list) {
|
||||
equal(list.length, 1, "There should be only one result");
|
||||
equal(list[0].id, parent2.id,
|
||||
"Should have gotten back the right result");
|
||||
"Should have gotten back the right result");
|
||||
done();
|
||||
}
|
||||
});
|
||||
@@ -246,6 +246,133 @@ describe('Parse.Relation testing', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("queries on relation fields with multiple ins", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
childObjects.push(new ChildObject({x: i}));
|
||||
}
|
||||
|
||||
Parse.Object.saveAll(childObjects).then(() => {
|
||||
var ParentObject = Parse.Object.extend("ParentObject");
|
||||
var parent = new ParentObject();
|
||||
parent.set("x", 4);
|
||||
var relation = parent.relation("child");
|
||||
relation.add(childObjects[0]);
|
||||
relation.add(childObjects[1]);
|
||||
relation.add(childObjects[2]);
|
||||
var parent2 = new ParentObject();
|
||||
parent2.set("x", 3);
|
||||
var relation2 = parent2.relation("child");
|
||||
relation2.add(childObjects[4]);
|
||||
relation2.add(childObjects[5]);
|
||||
relation2.add(childObjects[6]);
|
||||
|
||||
var otherChild2 = parent2.relation("otherChild");
|
||||
otherChild2.add(childObjects[0]);
|
||||
otherChild2.add(childObjects[1]);
|
||||
otherChild2.add(childObjects[2]);
|
||||
|
||||
var parents = [];
|
||||
parents.push(parent);
|
||||
parents.push(parent2);
|
||||
return Parse.Object.saveAll(parents);
|
||||
}).then(() => {
|
||||
var query = new Parse.Query(ParentObject);
|
||||
var objects = [];
|
||||
objects.push(childObjects[0]);
|
||||
query.containedIn("child", objects);
|
||||
query.containedIn("otherChild", [childObjects[0]]);
|
||||
return query.find();
|
||||
}).then((list) => {
|
||||
equal(list.length, 2, "There should be 2 results");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("query on pointer and relation fields with equal", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
childObjects.push(new ChildObject({x: i}));
|
||||
}
|
||||
|
||||
Parse.Object.saveAll(childObjects).then(() => {
|
||||
var ParentObject = Parse.Object.extend("ParentObject");
|
||||
var parent = new ParentObject();
|
||||
parent.set("x", 4);
|
||||
var relation = parent.relation("toChilds");
|
||||
relation.add(childObjects[0]);
|
||||
relation.add(childObjects[1]);
|
||||
relation.add(childObjects[2]);
|
||||
|
||||
var parent2 = new ParentObject();
|
||||
parent2.set("x", 3);
|
||||
parent2.set("toChild", childObjects[2]);
|
||||
|
||||
var parents = [];
|
||||
parents.push(parent);
|
||||
parents.push(parent2);
|
||||
parents.push(new ParentObject());
|
||||
|
||||
return Parse.Object.saveAll(parents).then(() => {
|
||||
var query = new Parse.Query(ParentObject);
|
||||
query.equalTo("objectId", parent.id);
|
||||
query.equalTo("toChilds", childObjects[2]);
|
||||
|
||||
return query.find().then((list) => {
|
||||
equal(list.length, 1, "There should be 1 result");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("or queries on pointer and relation fields", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
childObjects.push(new ChildObject({x: i}));
|
||||
}
|
||||
|
||||
Parse.Object.saveAll(childObjects).then(() => {
|
||||
var ParentObject = Parse.Object.extend("ParentObject");
|
||||
var parent = new ParentObject();
|
||||
parent.set("x", 4);
|
||||
var relation = parent.relation("toChilds");
|
||||
relation.add(childObjects[0]);
|
||||
relation.add(childObjects[1]);
|
||||
relation.add(childObjects[2]);
|
||||
|
||||
var parent2 = new ParentObject();
|
||||
parent2.set("x", 3);
|
||||
parent2.set("toChild", childObjects[2]);
|
||||
|
||||
var parents = [];
|
||||
parents.push(parent);
|
||||
parents.push(parent2);
|
||||
parents.push(new ParentObject());
|
||||
|
||||
return Parse.Object.saveAll(parents).then(() => {
|
||||
var query1 = new Parse.Query(ParentObject);
|
||||
query1.containedIn("toChilds", [childObjects[2]]);
|
||||
var query2 = new Parse.Query(ParentObject);
|
||||
query2.equalTo("toChild", childObjects[2]);
|
||||
var query = Parse.Query.or(query1, query2);
|
||||
return query.find().then((list) => {
|
||||
var objectIds = list.map(function(item){
|
||||
return item.id;
|
||||
});
|
||||
expect(objectIds.indexOf(parent.id)).not.toBe(-1);
|
||||
expect(objectIds.indexOf(parent2.id)).not.toBe(-1);
|
||||
equal(list.length, 2, "There should be 2 results");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("Get query on relation using un-fetched parent object", (done) => {
|
||||
// Setup data model
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
// Roles are not accessible without the master key, so they are not intended
|
||||
// for use by clients. We can manually test them using the master key.
|
||||
var Auth = require("../src/Auth").Auth;
|
||||
var Config = require("../src/Config");
|
||||
|
||||
describe('Parse Role testing', () => {
|
||||
|
||||
@@ -58,5 +60,64 @@ describe('Parse Role testing', () => {
|
||||
|
||||
});
|
||||
|
||||
it("should recursively load roles", (done) => {
|
||||
|
||||
var rolesNames = ["FooRole", "BarRole", "BazRole"];
|
||||
|
||||
var createRole = function(name, parent, user) {
|
||||
var role = new Parse.Role(name, new Parse.ACL());
|
||||
if (user) {
|
||||
var users = role.relation('users');
|
||||
users.add(user);
|
||||
}
|
||||
if (parent) {
|
||||
role.relation('roles').add(parent);
|
||||
}
|
||||
return role.save({}, { useMasterKey: true });
|
||||
}
|
||||
var roleIds = {};
|
||||
createTestUser().then( (user) => {
|
||||
|
||||
return createRole(rolesNames[0], null, null).then( (aRole) => {
|
||||
roleIds[aRole.get("name")] = aRole.id;
|
||||
return createRole(rolesNames[1], aRole, null);
|
||||
}).then( (anotherRole) => {
|
||||
roleIds[anotherRole.get("name")] = anotherRole.id;
|
||||
return createRole(rolesNames[2], anotherRole, user);
|
||||
}).then( (lastRole) => {
|
||||
roleIds[lastRole.get("name")] = lastRole.id;
|
||||
var auth = new Auth({ config: new Config("test"), isMaster: true, user: user });
|
||||
return auth._loadRoles();
|
||||
})
|
||||
}).then( (roles) => {
|
||||
expect(roles.length).toEqual(3);
|
||||
rolesNames.forEach( (name) => {
|
||||
expect(roles.indexOf('role:'+name)).not.toBe(-1);
|
||||
})
|
||||
done();
|
||||
}, function(err){
|
||||
fail("should succeed")
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("_Role object should not save without name.", (done) => {
|
||||
var role = new Parse.Role();
|
||||
role.save(null,{useMasterKey:true})
|
||||
.then((r) => {
|
||||
fail("_Role object should not save without name.");
|
||||
}, (error) => {
|
||||
expect(error.code).toEqual(111);
|
||||
role.set('name','testRole');
|
||||
role.save(null,{useMasterKey:true})
|
||||
.then((r2)=>{
|
||||
fail("_Role object should not save without ACL.");
|
||||
}, (error2) =>{
|
||||
expect(error2.code).toEqual(111);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -54,6 +54,11 @@ describe('Parse.User testing', () => {
|
||||
success: function(user) {
|
||||
Parse.User.logIn("non_existent_user", "asdf3",
|
||||
expectError(Parse.Error.OBJECT_NOT_FOUND, done));
|
||||
},
|
||||
error: function(err) {
|
||||
console.error(err);
|
||||
fail("Shit should not fail");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1026,6 +1031,32 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("login with provider should not call beforeSave trigger", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(model) {
|
||||
Parse.User.logOut();
|
||||
|
||||
Parse.Cloud.beforeSave(Parse.User, function(req, res) {
|
||||
res.error("Before save shouldn't be called on login");
|
||||
});
|
||||
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(innerModel) {
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
|
||||
done();
|
||||
},
|
||||
error: function(model, error) {
|
||||
ok(undefined, error);
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("link with provider", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
@@ -1678,7 +1709,7 @@ describe('Parse.User testing', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('test parse user become', (done) => {
|
||||
var sessionToken = null;
|
||||
Parse.Promise.as().then(function() {
|
||||
@@ -1732,5 +1763,22 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("session expiresAt correct format", (done) => {
|
||||
Parse.User.signUp("asdf", "zxcv", null, {
|
||||
success: function(user) {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/_Session',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
expect(body.results[0].expiresAt.__type).toEqual('Date');
|
||||
done();
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
26
spec/PromiseRouter.spec.js
Normal file
26
spec/PromiseRouter.spec.js
Normal file
@@ -0,0 +1,26 @@
|
||||
var PromiseRouter = require("../src/PromiseRouter").default;
|
||||
|
||||
describe("PromiseRouter", () => {
|
||||
|
||||
it("should properly handle rejects", (done) => {
|
||||
var router = new PromiseRouter();
|
||||
router.route("GET", "/dummy", (req)=> {
|
||||
return Promise.reject({
|
||||
error: "an error",
|
||||
code: -1
|
||||
})
|
||||
}, (req) => {
|
||||
fail("this should not be called");
|
||||
});
|
||||
|
||||
router.routes[0].handler({}).then((result) => {
|
||||
console.error(result);
|
||||
fail("this should not be called");
|
||||
done();
|
||||
}, (error)=> {
|
||||
expect(error.error).toEqual("an error");
|
||||
expect(error.code).toEqual(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
86
spec/PublicAPI.spec.js
Normal file
86
spec/PublicAPI.spec.js
Normal file
@@ -0,0 +1,86 @@
|
||||
|
||||
var request = require('request');
|
||||
|
||||
describe("public API", () => {
|
||||
beforeEach(done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
})
|
||||
it("should get invalid_link.html", (done) => {
|
||||
request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get choose_password", (done) => {
|
||||
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get verify_email_success.html", (done) => {
|
||||
request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get password_reset_success.html", (done) => {
|
||||
request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("public API without publicServerURL", () => {
|
||||
beforeEach(done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
});
|
||||
done();
|
||||
})
|
||||
it("should get 404 on verify_email", (done) => {
|
||||
request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 404 choose_password", (done) => {
|
||||
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 404 on request_password_reset", (done) => {
|
||||
request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
var PushController = require('../src/Controllers/PushController').PushController;
|
||||
|
||||
var Config = require('../src/Config');
|
||||
|
||||
describe('PushController', () => {
|
||||
it('can check valid master key of request', (done) => {
|
||||
// Make mock request
|
||||
@@ -127,5 +129,121 @@ describe('PushController', () => {
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('properly increment badges', (done) => {
|
||||
|
||||
var payload = {
|
||||
alert: "Hello World!",
|
||||
badge: "Increment",
|
||||
}
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
while(installations.length != 15) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("deviceType", "android");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.badge;
|
||||
installations.forEach((installation) => {
|
||||
if (installation.deviceType == "ios") {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(installation.originalBadge+1).toEqual(installation.badge);
|
||||
} else {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
})
|
||||
return Promise.resolve({
|
||||
body: body,
|
||||
installations: installations
|
||||
})
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios", "android"];
|
||||
}
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('properly set badges to 1', (done) => {
|
||||
|
||||
var payload = {
|
||||
alert: "Hello World!",
|
||||
badge: 1,
|
||||
}
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.badge;
|
||||
installations.forEach((installation) => {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(1).toEqual(installation.badge);
|
||||
})
|
||||
return Promise.resolve({
|
||||
body: body,
|
||||
installations: installations
|
||||
})
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
var Config = require('../src/Config');
|
||||
var Schema = require('../src/Schema');
|
||||
var dd = require('deep-diff');
|
||||
@@ -186,8 +188,8 @@ describe('Schema', () => {
|
||||
foo: {type: 'String'}
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME)
|
||||
expect(error.error).toEqual('class NewClass already exists');
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(error.message).toEqual('Class NewClass already exists.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -214,7 +216,7 @@ describe('Schema', () => {
|
||||
Promise.all([p1,p2])
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(error.error).toEqual('class NewClass already exists');
|
||||
expect(error.message).toEqual('Class NewClass already exists.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -420,6 +422,43 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates non-custom classes which include relation field', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Role', {}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: '_Role',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
objectId: 'string',
|
||||
name: 'string',
|
||||
users: 'relation<_User>',
|
||||
roles: 'relation<_Role>',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates non-custom classes which include pointer field', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Session', {}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: '_Session',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
objectId: 'string',
|
||||
restricted: 'boolean',
|
||||
user: '*_User',
|
||||
installationId: 'string',
|
||||
sessionToken: 'string',
|
||||
expiresAt: 'date',
|
||||
createdWith: 'object'
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to create two geopoints', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
@@ -483,7 +522,7 @@ describe('Schema', () => {
|
||||
.then(schema => schema.deleteField('installationId', '_Installation'))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(136);
|
||||
expect(error.error).toEqual('field installationId cannot be changed');
|
||||
expect(error.message).toEqual('field installationId cannot be changed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -493,7 +532,7 @@ describe('Schema', () => {
|
||||
.then(schema => schema.deleteField('field', 'NoClass'))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(error.error).toEqual('class NoClass does not exist');
|
||||
expect(error.message).toEqual('Class NoClass does not exist.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -504,7 +543,7 @@ describe('Schema', () => {
|
||||
.then(schema => schema.deleteField('missingField', 'HasAllPOD'))
|
||||
.fail(error => {
|
||||
expect(error.code).toEqual(255);
|
||||
expect(error.error).toEqual('field missingField does not exist, cannot delete');
|
||||
expect(error.message).toEqual('Field missingField does not exist, cannot delete.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -512,24 +551,32 @@ describe('Schema', () => {
|
||||
it('drops related collection when deleting relation field', done => {
|
||||
var obj1 = hasAllPODobject();
|
||||
obj1.save()
|
||||
.then(savedObj1 => {
|
||||
var obj2 = new Parse.Object('HasPointersAndRelations');
|
||||
obj2.set('aPointer', savedObj1);
|
||||
var relation = obj2.relation('aRelation');
|
||||
relation.add(obj1);
|
||||
return obj2.save();
|
||||
})
|
||||
.then(() => {
|
||||
config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
|
||||
expect(err).toEqual(null);
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database.db, 'test_'))
|
||||
.then(() => config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
|
||||
expect(err).not.toEqual(null);
|
||||
done();
|
||||
}))
|
||||
.then(savedObj1 => {
|
||||
var obj2 = new Parse.Object('HasPointersAndRelations');
|
||||
obj2.set('aPointer', savedObj1);
|
||||
var relation = obj2.relation('aRelation');
|
||||
relation.add(obj1);
|
||||
return obj2.save();
|
||||
})
|
||||
.then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations'))
|
||||
.then(exists => {
|
||||
if (!exists) {
|
||||
fail('Relation collection ' +
|
||||
'should exist after save.');
|
||||
}
|
||||
})
|
||||
.then(() => config.database.loadSchema())
|
||||
.then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database))
|
||||
.then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations'))
|
||||
.then(exists => {
|
||||
if (exists) {
|
||||
fail('Relation collection should not exist after deleting relation field.');
|
||||
}
|
||||
done();
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('can delete string fields and resave as number field', done => {
|
||||
@@ -538,7 +585,7 @@ describe('Schema', () => {
|
||||
var obj2 = hasAllPODobject();
|
||||
var p = Parse.Object.saveAll([obj1, obj2])
|
||||
.then(() => config.database.loadSchema())
|
||||
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database.db, 'test_'))
|
||||
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database))
|
||||
.then(() => new Parse.Query('HasAllPOD').get(obj1.id))
|
||||
.then(obj1Reloaded => {
|
||||
expect(obj1Reloaded.get('aString')).toEqual(undefined);
|
||||
@@ -568,7 +615,7 @@ describe('Schema', () => {
|
||||
expect(obj1.get('aPointer').id).toEqual(obj1.id);
|
||||
})
|
||||
.then(() => config.database.loadSchema())
|
||||
.then(schema => schema.deleteField('aPointer', 'NewClass', config.database.db, 'test_'))
|
||||
.then(schema => schema.deleteField('aPointer', 'NewClass', config.database))
|
||||
.then(() => new Parse.Query('NewClass').get(obj1.id))
|
||||
.then(obj1 => {
|
||||
expect(obj1.get('aPointer')).toEqual(undefined);
|
||||
@@ -609,4 +656,21 @@ describe('Schema', () => {
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('ignore default field when merge with system class', done => {
|
||||
expect(Schema.buildMergedSchemaObject({
|
||||
_id: '_User',
|
||||
username: 'string',
|
||||
password: 'string',
|
||||
authData: 'object',
|
||||
email: 'string',
|
||||
emailVerified: 'boolean'
|
||||
},{
|
||||
authData: {type: 'string'},
|
||||
customField: {type: 'string'},
|
||||
})).toEqual({
|
||||
customField: {type: 'string'}
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
618
spec/ValidationAndPasswordsReset.spec.js
Normal file
618
spec/ValidationAndPasswordsReset.spec.js
Normal file
@@ -0,0 +1,618 @@
|
||||
"use strict";
|
||||
|
||||
var request = require('request');
|
||||
var Config = require("../src/Config");
|
||||
describe("Custom Pages Configuration", () => {
|
||||
it("should set the custom pages", (done) => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
customPages: {
|
||||
invalidLink: "myInvalidLink",
|
||||
verifyEmailSuccess: "myVerifyEmailSuccess",
|
||||
choosePassword: "myChoosePassword",
|
||||
passwordResetSuccess: "myPasswordResetSuccess"
|
||||
},
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
});
|
||||
|
||||
var config = new Config("test");
|
||||
|
||||
expect(config.invalidLinkURL).toEqual("myInvalidLink");
|
||||
expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
|
||||
expect(config.choosePasswordURL).toEqual("myChoosePassword");
|
||||
expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
|
||||
expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
|
||||
expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Email Verification", () => {
|
||||
it('sends verification email if email verification is enabled', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.setEmail('cool_guy@parse.com');
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send verification email when verification is enabled and email is not set', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does send a validation email when updating the email', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
user.set("email", "cool_guy@parse.com");
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return user.fetch();
|
||||
}).then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
// Wait as on update emai, we need to fetch the username
|
||||
setTimeout(function(){
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does send with a simple adapter', done => {
|
||||
var calls = 0;
|
||||
var emailAdapter = {
|
||||
sendMail: function(options){
|
||||
expect(options.to).toBe('cool_guy@parse.com');
|
||||
if (calls == 0) {
|
||||
expect(options.subject).toEqual('Please verify your e-mail for My Cool App');
|
||||
expect(options.text.match(/verify_email/)).not.toBe(null);
|
||||
} else if (calls == 1) {
|
||||
expect(options.subject).toEqual('Password Reset for My Cool App');
|
||||
expect(options.text.match(/request_password_reset/)).not.toBe(null);
|
||||
}
|
||||
calls++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'My Cool App',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set("email", "cool_guy@parse.com");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(calls).toBe(1);
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return Parse.User.requestPasswordReset("cool_guy@parse.com");
|
||||
}).then(() => {
|
||||
expect(calls).toBe(2);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send verification email if email verification is disabled', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: false,
|
||||
emailAdapter: emailAdapter,
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0);
|
||||
expect(user.get('emailVerified')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('receives the app name and user in the adapter', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
expect(options.appName).toEqual('emailing app');
|
||||
expect(options.user.get('email')).toEqual('user@parse.com');
|
||||
done();
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp(null, {
|
||||
success: () => {},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
it('when you click the link in the email it sets emailVerified to true and redirects you', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
request.get(options.link, {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=zxcv');
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(true);
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("this should not fail");
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp();
|
||||
});
|
||||
|
||||
it('redirects you to invalid link if you try to verify email incorrecly', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done()
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects you to invalid link if you try to validate a nonexistant users email', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update email verified if you use an invalid token', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp(null, {
|
||||
success: () => {},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Password Reset", () => {
|
||||
|
||||
it('should send a password reset link', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
request.get(options.link, {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
fail("Failed to get the reset link");
|
||||
return;
|
||||
}
|
||||
expect(response.statusCode).toEqual(302);
|
||||
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv/;
|
||||
expect(response.body.match(re)).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
},
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user@parse.com', {
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects you to invalid link if you try to request password for a nonexistant users email', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should programatically reset password', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
request.get(options.link, {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
fail("Failed to get the reset link");
|
||||
return;
|
||||
}
|
||||
expect(response.statusCode).toEqual(302);
|
||||
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/;
|
||||
var match = response.body.match(re);
|
||||
if (!match) {
|
||||
fail("should have a token");
|
||||
done();
|
||||
return;
|
||||
}
|
||||
var token = match[1];
|
||||
|
||||
request.post({
|
||||
url: "http://localhost:8378/1/apps/test/request_password_reset" ,
|
||||
body: `new_password=hello&token=${token}&username=zxcv`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
fail("Failed to POST request password reset");
|
||||
return;
|
||||
}
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html');
|
||||
|
||||
Parse.User.logIn("zxcv", "hello").then(function(user){
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should login with new password");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
},
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user@parse.com', {
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
@@ -100,3 +100,11 @@ Parse.Cloud.define('requiredParameterCheck', function(req, res) {
|
||||
}, function(params) {
|
||||
return params.name;
|
||||
});
|
||||
|
||||
Parse.Cloud.define('echoKeys', function(req, res){
|
||||
return res.success({
|
||||
applicationId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
javascriptKey: Parse.javascriptKey
|
||||
})
|
||||
});
|
||||
|
||||
44
spec/features.spec.js
Normal file
44
spec/features.spec.js
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
var features = require('../src/features');
|
||||
const request = require("request");
|
||||
|
||||
describe('features', () => {
|
||||
it('set and get features', (done) => {
|
||||
features.setFeature('push', {
|
||||
testOption1: true,
|
||||
testOption2: false
|
||||
});
|
||||
|
||||
var _features = features.getFeatures();
|
||||
|
||||
var expected = {
|
||||
testOption1: true,
|
||||
testOption2: false
|
||||
};
|
||||
|
||||
expect(_features.push).toEqual(expected);
|
||||
done();
|
||||
});
|
||||
|
||||
it('get features that does not exist', (done) => {
|
||||
var _features = features.getFeatures();
|
||||
expect(_features.test).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
it('requires the master key to get all schemas', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -52,13 +52,13 @@ delete defaultConfiguration.cloud;
|
||||
|
||||
// Allows testing specific configurations of Parse Server
|
||||
var setServerConfiguration = configuration => {
|
||||
api = new ParseServer(configuration);
|
||||
app = express();
|
||||
app.use('/1', api);
|
||||
cache.clearCache();
|
||||
server.close();
|
||||
cache.clearCache();
|
||||
app = express();
|
||||
api = new ParseServer(configuration);
|
||||
app.use('/1', api);
|
||||
server = app.listen(port);
|
||||
}
|
||||
};
|
||||
|
||||
var restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration);
|
||||
|
||||
@@ -250,3 +250,4 @@ global.arrayContains = arrayContains;
|
||||
global.jequal = jequal;
|
||||
global.range = range;
|
||||
global.setServerConfiguration = setServerConfiguration;
|
||||
global.defaultConfiguration = defaultConfiguration;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
var request = require('request');
|
||||
var parseServerPackage = require('../package.json');
|
||||
var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
|
||||
|
||||
describe('server', () => {
|
||||
it('requires a master key and app id', done => {
|
||||
@@ -37,4 +39,133 @@ describe('server', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can load email adapter via object', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can load email adapter via class', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
class: MockEmailAdapterWithOptions,
|
||||
options: {
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can load email adapter via module name', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
module: './Email/SimpleMailgunAdapter',
|
||||
options: {
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can load email adapter via only module name', done => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: './Email/SimpleMailgunAdapter',
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})).toThrow('SimpleMailgunAdapter requires an API Key and domain.');
|
||||
done();
|
||||
});
|
||||
|
||||
it('throws if you initialize email adapter incorrecly', done => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
module: './Email/SimpleMailgunAdapter',
|
||||
options: {
|
||||
domain: 'd',
|
||||
}
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})).toThrow('SimpleMailgunAdapter requires an API Key and domain.');
|
||||
done();
|
||||
});
|
||||
|
||||
it('can report the server version', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(body.parseServerVersion).toEqual(parseServerPackage.version);
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
var Parse = require('parse/node').Parse;
|
||||
var request = require('request');
|
||||
var dd = require('deep-diff');
|
||||
@@ -96,8 +98,8 @@ describe('schemas', () => {
|
||||
json: true,
|
||||
headers: restKeyHeaders,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(body.error).toEqual('master key not specified');
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -108,8 +110,8 @@ describe('schemas', () => {
|
||||
json: true,
|
||||
headers: restKeyHeaders,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(body.error).toEqual('master key not specified');
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -173,7 +175,7 @@ describe('schemas', () => {
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body).toEqual({
|
||||
code: 103,
|
||||
error: 'class HASALLPOD does not exist',
|
||||
error: 'Class HASALLPOD does not exist.',
|
||||
});
|
||||
done();
|
||||
});
|
||||
@@ -204,8 +206,8 @@ describe('schemas', () => {
|
||||
className: 'MyClass',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(body.error).toEqual('master key not specified');
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -222,7 +224,7 @@ describe('schemas', () => {
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body).toEqual({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class name mismatch between B and A',
|
||||
error: 'Class name mismatch between B and A.',
|
||||
});
|
||||
done();
|
||||
});
|
||||
@@ -238,7 +240,7 @@ describe('schemas', () => {
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body).toEqual({
|
||||
code: 135,
|
||||
error: 'POST /schemas needs class name',
|
||||
error: 'POST /schemas needs a class name.',
|
||||
});
|
||||
done();
|
||||
})
|
||||
@@ -265,7 +267,7 @@ describe('schemas', () => {
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body).toEqual({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class A already exists',
|
||||
error: 'Class A already exists.'
|
||||
});
|
||||
done();
|
||||
});
|
||||
@@ -351,7 +353,7 @@ describe('schemas', () => {
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(body.error).toEqual('class name mismatch between WrongClassName and NewClass');
|
||||
expect(body.error).toEqual('Class name mismatch between WrongClassName and NewClass.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -369,7 +371,7 @@ describe('schemas', () => {
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(body.error).toEqual('class NoClass does not exist');
|
||||
expect(body.error).toEqual('Class NoClass does not exist.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -390,13 +392,13 @@ describe('schemas', () => {
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body.code).toEqual(255);
|
||||
expect(body.error).toEqual('field aString exists, cannot update');
|
||||
expect(body.error).toEqual('Field aString exists, cannot update.');
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('refuses to delete non-existant fields', done => {
|
||||
it('refuses to delete non-existent fields', done => {
|
||||
var obj = hasAllPODobject();
|
||||
obj.save()
|
||||
.then(() => {
|
||||
@@ -406,13 +408,13 @@ describe('schemas', () => {
|
||||
json: true,
|
||||
body: {
|
||||
fields: {
|
||||
nonExistantKey: {__op: "Delete"},
|
||||
nonExistentKey: {__op: "Delete"},
|
||||
}
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body.code).toEqual(255);
|
||||
expect(body.error).toEqual('field nonExistantKey does not exist, cannot delete');
|
||||
expect(body.error).toEqual('Field nonExistentKey does not exist, cannot delete.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -660,7 +662,8 @@ describe('schemas', () => {
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body.code).toEqual(255);
|
||||
expect(body.error).toEqual('class HasAllPOD not empty, contains 1 objects, cannot drop schema');
|
||||
expect(body.error).toMatch(/HasAllPOD/);
|
||||
expect(body.error).toMatch(/contains 1/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -710,28 +713,106 @@ describe('schemas', () => {
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toEqual({});
|
||||
config.database.db.collection('test__Join:aRelation:MyOtherClass', { strict: true }, (err, coll) => {
|
||||
//Expect Join table to be gone
|
||||
expect(err).not.toEqual(null);
|
||||
config.database.db.collection('test_MyOtherClass', { strict: true }, (err, coll) => {
|
||||
// Expect data table to be gone
|
||||
expect(err).not.toEqual(null);
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/schemas/MyOtherClass',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
//Expect _SCHEMA entry to be gone.
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(body.error).toEqual('class MyOtherClass does not exist');
|
||||
done();
|
||||
config.database.collectionExists('_Join:aRelation:MyOtherClass').then(exists => {
|
||||
if (exists) {
|
||||
fail('Relation collection should be deleted.');
|
||||
done();
|
||||
}
|
||||
return config.database.collectionExists('MyOtherClass');
|
||||
}).then(exists => {
|
||||
if (exists) {
|
||||
fail('Class collection should be deleted.');
|
||||
done();
|
||||
}
|
||||
}).then(() => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/schemas/MyOtherClass',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
//Expect _SCHEMA entry to be gone.
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(body.error).toEqual('Class MyOtherClass does not exist.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}).then(() => {
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes schema when actual collection does not exist', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
body: {
|
||||
className: 'NewClassForDelete'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(error).toEqual(null);
|
||||
expect(response.body.className).toEqual('NewClassForDelete');
|
||||
request.del({
|
||||
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toEqual({});
|
||||
config.database.loadSchema().then(schema => {
|
||||
schema.hasClass('NewClassForDelete').then(exist => {
|
||||
expect(exist).toEqual(false);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes schema when actual collection exists', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
body: {
|
||||
className: 'NewClassForDelete'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(error).toEqual(null);
|
||||
expect(response.body.className).toEqual('NewClassForDelete');
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/NewClassForDelete',
|
||||
headers: restKeyHeaders,
|
||||
json: true
|
||||
}, (error, response, body) => {
|
||||
expect(error).toEqual(null);
|
||||
expect(typeof response.body.objectId).toEqual('string');
|
||||
request.del({
|
||||
url: 'http://localhost:8378/1/classes/NewClassForDelete/' + response.body.objectId,
|
||||
headers: restKeyHeaders,
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(error).toEqual(null);
|
||||
request.del({
|
||||
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toEqual({});
|
||||
config.database.loadSchema().then(schema => {
|
||||
schema.hasClass('NewClassForDelete').then(exist => {
|
||||
expect(exist).toEqual(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}, error => {
|
||||
fail(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
export function loadAdapter(adapter, defaultAdapter, options) {
|
||||
|
||||
export function loadAdapter(options, defaultAdapter) {
|
||||
let adapter;
|
||||
|
||||
// We have options and options have adapter key
|
||||
if (options) {
|
||||
// Pass an adapter as a module name, a function or an instance
|
||||
if (typeof options == "string" || typeof options == "function" || options.constructor != Object) {
|
||||
adapter = options;
|
||||
if (!adapter)
|
||||
{
|
||||
if (!defaultAdapter) {
|
||||
return options;
|
||||
}
|
||||
if (options.adapter) {
|
||||
adapter = options.adapter;
|
||||
// Load from the default adapter when no adapter is set
|
||||
return loadAdapter(defaultAdapter, undefined, options);
|
||||
} else if (typeof adapter === "function") {
|
||||
try {
|
||||
return adapter(options);
|
||||
} catch(e) {
|
||||
var Adapter = adapter;
|
||||
return new Adapter(options);
|
||||
}
|
||||
}
|
||||
|
||||
if (!adapter) {
|
||||
adapter = defaultAdapter;
|
||||
}
|
||||
|
||||
// This is a string, require the module
|
||||
if (typeof adapter === "string") {
|
||||
} else if (typeof adapter === "string") {
|
||||
adapter = require(adapter);
|
||||
// If it's define as a module, get the default
|
||||
if (adapter.default) {
|
||||
adapter = adapter.default;
|
||||
}
|
||||
|
||||
return loadAdapter(adapter, undefined, options);
|
||||
} else if (adapter.module) {
|
||||
return loadAdapter(adapter.module, undefined, adapter.options);
|
||||
} else if (adapter.class) {
|
||||
return loadAdapter(adapter.class, undefined, adapter.options);
|
||||
} else if (adapter.adapter) {
|
||||
return loadAdapter(adapter.adapter, undefined, adapter.options);
|
||||
} else {
|
||||
// Try to load the defaultAdapter with the options
|
||||
// The default adapter should throw if the options are
|
||||
// incompatible
|
||||
try {
|
||||
return loadAdapter(defaultAdapter, undefined, adapter);
|
||||
} catch (e) {};
|
||||
}
|
||||
// From there it's either a function or an object
|
||||
// if it's an function, instanciate and pass the options
|
||||
if (typeof adapter === "function") {
|
||||
var Adapter = adapter;
|
||||
adapter = new Adapter(options);
|
||||
}
|
||||
return adapter;
|
||||
// return the adapter as is as it's unusable otherwise
|
||||
return adapter;
|
||||
}
|
||||
|
||||
export default loadAdapter;
|
||||
|
||||
23
src/Adapters/Email/MailAdapter.js
Normal file
23
src/Adapters/Email/MailAdapter.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
/*
|
||||
Mail Adapter prototype
|
||||
A MailAdapter should implement at least sendMail()
|
||||
*/
|
||||
export class MailAdapter {
|
||||
/*
|
||||
* A method for sending mail
|
||||
* @param options would have the parameters
|
||||
* - to: the recipient
|
||||
* - text: the raw text of the message
|
||||
* - subject: the subject of the email
|
||||
*/
|
||||
sendMail(options) {}
|
||||
|
||||
/* You can implement those methods if you want
|
||||
* to provide HTML templates etc...
|
||||
*/
|
||||
// sendVerificationEmail({ link, appName, user }) {}
|
||||
// sendPasswordResetEmail({ link, appName, user }) {}
|
||||
}
|
||||
|
||||
export default MailAdapter;
|
||||
32
src/Adapters/Email/SimpleMailgunAdapter.js
Normal file
32
src/Adapters/Email/SimpleMailgunAdapter.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import Mailgun from 'mailgun-js';
|
||||
|
||||
let SimpleMailgunAdapter = mailgunOptions => {
|
||||
if (!mailgunOptions || !mailgunOptions.apiKey || !mailgunOptions.domain) {
|
||||
throw 'SimpleMailgunAdapter requires an API Key and domain.';
|
||||
}
|
||||
let mailgun = Mailgun(mailgunOptions);
|
||||
|
||||
let sendMail = ({to, subject, text}) => {
|
||||
let data = {
|
||||
from: mailgunOptions.fromAddress,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: text,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
mailgun.messages().send(data, (err, body) => {
|
||||
if (typeof err !== 'undefined') {
|
||||
reject(err);
|
||||
}
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
sendMail: sendMail
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SimpleMailgunAdapter
|
||||
@@ -8,11 +8,23 @@
|
||||
// * getFileLocation(config, request, filename)
|
||||
//
|
||||
// Default is GridStoreAdapter, which requires mongo
|
||||
// and for the API server to be using the ExportAdapter
|
||||
// and for the API server to be using the DatabaseController with Mongo
|
||||
// database adapter.
|
||||
|
||||
export class FilesAdapter {
|
||||
createFile(config, filename, data) { }
|
||||
/* this method is responsible to store the file in order to be retrived later by it's file name
|
||||
*
|
||||
*
|
||||
* @param config the current config
|
||||
* @param filename the filename to save
|
||||
* @param data the buffer of data from the file
|
||||
* @param contentType the supposed contentType
|
||||
* @discussion the contentType can be undefined if the controller was not able to determine it
|
||||
*
|
||||
* @return a promise that should fail if the storage didn't succeed
|
||||
*
|
||||
*/
|
||||
createFile(config, filename: string, data, contentType: string) { }
|
||||
|
||||
deleteFile(config, filename) { }
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// GCSAdapter
|
||||
// Store Parse Files in Google Cloud Storage: https://cloud.google.com/storage
|
||||
import * as gcloud from 'gcloud';
|
||||
import { storage } from 'gcloud';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
import requiredParameter from '../../requiredParameter';
|
||||
|
||||
export class GCSAdapter extends FilesAdapter {
|
||||
// GCS Project ID and the name of a corresponding Keyfile are required.
|
||||
// Unlike the S3 adapter, you must create a new Cloud Storage bucket, as this is not created automatically.
|
||||
// See https://googlecloudplatform.github.io/gcloud-node/#/docs/master/guides/authentication
|
||||
// for more details.
|
||||
constructor(
|
||||
projectId,
|
||||
keyFilename,
|
||||
bucket,
|
||||
projectId = requiredParameter('GCSAdapter requires a GCP Project ID'),
|
||||
keyFilename = requiredParameter('GCSAdapter requires a GCP keyfile'),
|
||||
bucket = requiredParameter('GCSAdapter requires a GCS bucket name'),
|
||||
{ bucketPrefix = '',
|
||||
directAccess = false } = {}
|
||||
) {
|
||||
@@ -20,21 +22,25 @@ export class GCSAdapter extends FilesAdapter {
|
||||
this._bucketPrefix = bucketPrefix;
|
||||
this._directAccess = directAccess;
|
||||
|
||||
let gcsOptions = {
|
||||
let options = {
|
||||
projectId: projectId,
|
||||
keyFilename: keyFilename
|
||||
};
|
||||
|
||||
this._gcsClient = new gcloud.storage(gcsOptions);
|
||||
this._gcsClient = new storage(options);
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file in GCS.
|
||||
// Resolves the promise or fails with an error.
|
||||
createFile(config, filename, data) {
|
||||
createFile(config, filename, data, contentType) {
|
||||
let params = {
|
||||
contentType: contentType || 'application/octet-stream'
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
|
||||
// gcloud supports upload(file) not upload(bytes), so we need to stream.
|
||||
var uploadStream = file.createWriteStream();
|
||||
var uploadStream = file.createWriteStream(params);
|
||||
uploadStream.on('error', (err) => {
|
||||
return reject(err);
|
||||
}).on('finish', () => {
|
||||
@@ -61,6 +67,7 @@ export class GCSAdapter extends FilesAdapter {
|
||||
return new Promise((resolve, reject) => {
|
||||
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
|
||||
file.delete((err, res) => {
|
||||
console.log("delete: ", filename, err, res);
|
||||
if(err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
@@ -74,11 +81,19 @@ export class GCSAdapter extends FilesAdapter {
|
||||
getFileData(config, filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
|
||||
file.download((err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
// Check for existence, since gcloud-node seemed to be caching the result
|
||||
file.exists((err, exists) => {
|
||||
if (exists) {
|
||||
file.download((err, data) => {
|
||||
console.log("get: ", filename, err, data);
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(data);
|
||||
});
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,28 +1,47 @@
|
||||
// GridStoreAdapter
|
||||
//
|
||||
// Stores files in Mongo using GridStore
|
||||
// Requires the database adapter to be based on mongoclient
|
||||
/**
|
||||
GridStoreAdapter
|
||||
Stores files in Mongo using GridStore
|
||||
Requires the database adapter to be based on mongoclient
|
||||
|
||||
import { GridStore } from 'mongodb';
|
||||
@flow weak
|
||||
*/
|
||||
|
||||
import { MongoClient, GridStore, Db} from 'mongodb';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
|
||||
export class GridStoreAdapter extends FilesAdapter {
|
||||
_databaseURI: string;
|
||||
_connectionPromise: Promise<Db>;
|
||||
|
||||
constructor(mongoDatabaseURI: string) {
|
||||
super();
|
||||
this._databaseURI = mongoDatabaseURI;
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_connect() {
|
||||
if (!this._connectionPromise) {
|
||||
this._connectionPromise = MongoClient.connect(this._databaseURI);
|
||||
}
|
||||
return this._connectionPromise;
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file
|
||||
// Returns a promise
|
||||
createFile(config, filename, data) {
|
||||
return config.database.connect().then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'w');
|
||||
createFile(config, filename: string, data, contentType) {
|
||||
return this._connect().then(database => {
|
||||
let gridStore = new GridStore(database, filename, 'w');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
}).then(gridStore => {
|
||||
return gridStore.write(data);
|
||||
}).then((gridStore) => {
|
||||
}).then(gridStore => {
|
||||
return gridStore.close();
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(config, filename) {
|
||||
return config.database.connect().then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'w');
|
||||
deleteFile(config, filename: string) {
|
||||
return this._connect().then(database => {
|
||||
let gridStore = new GridStore(database, filename, 'w');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
return gridStore.unlink();
|
||||
@@ -31,13 +50,14 @@ export class GridStoreAdapter extends FilesAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
getFileData(config, filename) {
|
||||
return config.database.connect().then(() => {
|
||||
return GridStore.exist(config.database.db, filename);
|
||||
}).then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'r');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
getFileData(config, filename: string) {
|
||||
return this._connect().then(database => {
|
||||
return GridStore.exist(database, filename)
|
||||
.then(() => {
|
||||
let gridStore = new GridStore(database, filename, 'r');
|
||||
return gridStore.open();
|
||||
});
|
||||
}).then(gridStore => {
|
||||
return gridStore.read();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,23 +4,38 @@
|
||||
|
||||
import * as AWS from 'aws-sdk';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
import requiredParameter from '../../requiredParameter';
|
||||
|
||||
const DEFAULT_S3_REGION = "us-east-1";
|
||||
|
||||
function parseS3AdapterOptions(...options) {
|
||||
if (options.length === 1 && typeof options[0] == "object") {
|
||||
return options;
|
||||
}
|
||||
|
||||
const additionalOptions = options[3] || {};
|
||||
|
||||
return {
|
||||
accessKey: options[0],
|
||||
secretKey: options[1],
|
||||
bucket: options[2],
|
||||
region: additionalOptions.region
|
||||
}
|
||||
}
|
||||
|
||||
export class S3Adapter extends FilesAdapter {
|
||||
// Creates an S3 session.
|
||||
// Providing AWS access and secret keys is mandatory
|
||||
// Region and bucket will use sane defaults if omitted
|
||||
constructor(
|
||||
accessKey,
|
||||
secretKey,
|
||||
bucket,
|
||||
{ region = DEFAULT_S3_REGION,
|
||||
bucketPrefix = '',
|
||||
directAccess = false } = {}
|
||||
) {
|
||||
accessKey = requiredParameter('S3Adapter requires an accessKey'),
|
||||
secretKey = requiredParameter('S3Adapter requires a secretKey'),
|
||||
bucket,
|
||||
{ region = DEFAULT_S3_REGION,
|
||||
bucketPrefix = '',
|
||||
directAccess = false } = {}) {
|
||||
super();
|
||||
|
||||
|
||||
this._region = region;
|
||||
this._bucket = bucket;
|
||||
this._bucketPrefix = bucketPrefix;
|
||||
@@ -33,11 +48,27 @@ export class S3Adapter extends FilesAdapter {
|
||||
};
|
||||
AWS.config._region = this._region;
|
||||
this._s3Client = new AWS.S3(s3Options);
|
||||
this._hasBucket = false;
|
||||
}
|
||||
|
||||
createBucket() {
|
||||
var promise;
|
||||
if (this._hasBucket) {
|
||||
promise = Promise.resolve();
|
||||
} else {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
this._s3Client.createBucket(() => {
|
||||
this._hasBucket = true;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file in S3
|
||||
// Returns a promise containing the S3 object creation response
|
||||
createFile(config, filename, data) {
|
||||
createFile(config, filename, data, contentType) {
|
||||
let params = {
|
||||
Key: this._bucketPrefix + filename,
|
||||
Body: data
|
||||
@@ -45,26 +76,33 @@ export class S3Adapter extends FilesAdapter {
|
||||
if (this._directAccess) {
|
||||
params.ACL = "public-read"
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.upload(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
if (contentType) {
|
||||
params.ContentType = contentType;
|
||||
}
|
||||
return this.createBucket().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.upload(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(config, filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let params = {
|
||||
Key: this._bucketPrefix + filename
|
||||
};
|
||||
this._s3Client.deleteObject(params, (err, data) =>{
|
||||
if(err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
return this.createBucket().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let params = {
|
||||
Key: this._bucketPrefix + filename
|
||||
};
|
||||
this._s3Client.deleteObject(params, (err, data) =>{
|
||||
if(err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -73,12 +111,18 @@ export class S3Adapter extends FilesAdapter {
|
||||
// Returns a promise that succeeds with the buffer result from S3
|
||||
getFileData(config, filename) {
|
||||
let params = {Key: this._bucketPrefix + filename};
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.getObject(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data.Body);
|
||||
return this.createBucket().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.getObject(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
// Something happend here...
|
||||
if (data && !data.Body) {
|
||||
return reject(data);
|
||||
}
|
||||
resolve(data.Body);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => {
|
||||
export class FileLoggerAdapter extends LoggerAdapter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
this._logsFolder = options.logsFolder || LOGS_FOLDER;
|
||||
|
||||
// check logs folder exists
|
||||
|
||||
@@ -18,6 +18,10 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
this.senderMap = {};
|
||||
this.OneSignalConfig = {};
|
||||
const { oneSignalAppId, oneSignalApiKey } = pushConfig;
|
||||
if (!oneSignalAppId || !oneSignalApiKey) {
|
||||
throw "Trying to initialize OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey";
|
||||
}
|
||||
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
|
||||
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ export class ParsePushAdapter extends PushAdapter {
|
||||
super(pushConfig);
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
this.senderMap = {};
|
||||
// used in PushController for Dashboard Features
|
||||
this.feature = {
|
||||
immediatePush: true
|
||||
};
|
||||
let pushTypes = Object.keys(pushConfig);
|
||||
|
||||
for (let pushType of pushTypes) {
|
||||
|
||||
76
src/Adapters/Storage/Mongo/MongoCollection.js
Normal file
76
src/Adapters/Storage/Mongo/MongoCollection.js
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
let mongodb = require('mongodb');
|
||||
let Collection = mongodb.Collection;
|
||||
|
||||
export default class MongoCollection {
|
||||
_mongoCollection:Collection;
|
||||
|
||||
constructor(mongoCollection:Collection) {
|
||||
this._mongoCollection = mongoCollection;
|
||||
}
|
||||
|
||||
// Does a find with "smart indexing".
|
||||
// Currently this just means, if it needs a geoindex and there is
|
||||
// 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 })
|
||||
.catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (error.code != 17007 ||
|
||||
!error.message.match(/unable to find index for .geoNear/)) {
|
||||
throw error;
|
||||
}
|
||||
// Figure out what key needs an index
|
||||
let key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
//TODO: condiser moving index creation logic into Schema.js
|
||||
return this._mongoCollection.createIndex(index)
|
||||
// Retry, but just once.
|
||||
.then(() => this._rawFind(query, { skip, limit, sort }));
|
||||
});
|
||||
}
|
||||
|
||||
_rawFind(query, { skip, limit, sort } = {}) {
|
||||
return this._mongoCollection
|
||||
.find(query, { skip, limit, sort })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
count(query, { skip, limit, sort } = {}) {
|
||||
return this._mongoCollection.count(query, { skip, limit, sort });
|
||||
}
|
||||
|
||||
// Atomically finds and updates an object based on query.
|
||||
// The result is the promise with an object that was in the database !AFTER! changes.
|
||||
// Postgres Note: Translates directly to `UPDATE * SET * ... RETURNING *`, which will return data after the change is done.
|
||||
findOneAndUpdate(query, update) {
|
||||
// arguments: query, sort, update, options(optional)
|
||||
// Setting `new` option to true makes it return the after document, not the before one.
|
||||
return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => {
|
||||
// Value is the object where mongo returns multiple fields.
|
||||
return document.value;
|
||||
})
|
||||
}
|
||||
|
||||
// Atomically find and delete an object based on query.
|
||||
// The result is the promise with an object that was in the database before deleting.
|
||||
// Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done.
|
||||
findOneAndDelete(query) {
|
||||
// arguments: query, sort
|
||||
return this._mongoCollection.findAndRemove(query, []).then(document => {
|
||||
// Value is the object where mongo returns multiple fields.
|
||||
return document.value;
|
||||
});
|
||||
}
|
||||
|
||||
drop() {
|
||||
return this._mongoCollection.drop();
|
||||
}
|
||||
}
|
||||
68
src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Normal file
68
src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
import MongoCollection from './MongoCollection';
|
||||
|
||||
let mongodb = require('mongodb');
|
||||
let MongoClient = mongodb.MongoClient;
|
||||
|
||||
export class MongoStorageAdapter {
|
||||
// Private
|
||||
_uri: string;
|
||||
// Public
|
||||
connectionPromise;
|
||||
database;
|
||||
|
||||
constructor(uri: string) {
|
||||
this._uri = uri;
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
this.connectionPromise = MongoClient.connect(this._uri).then(database => {
|
||||
this.database = database;
|
||||
});
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
collection(name: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.collection(name);
|
||||
});
|
||||
}
|
||||
|
||||
adaptiveCollection(name: string) {
|
||||
return this.connect()
|
||||
.then(() => this.database.collection(name))
|
||||
.then(rawCollection => new MongoCollection(rawCollection));
|
||||
}
|
||||
|
||||
collectionExists(name: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.listCollections({ name: name }).toArray();
|
||||
}).then(collections => {
|
||||
return collections.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
dropCollection(name: string) {
|
||||
return this.collection(name).then(collection => collection.drop());
|
||||
}
|
||||
// Used for testing only right now.
|
||||
collectionsContaining(match: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.collections();
|
||||
}).then(collections => {
|
||||
return collections.filter(collection => {
|
||||
if (collection.namespace.match(/\.system\./)) {
|
||||
return false;
|
||||
}
|
||||
return (collection.collectionName.indexOf(match) == 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default MongoStorageAdapter;
|
||||
module.exports = MongoStorageAdapter; // Required for tests
|
||||
37
src/Auth.js
37
src/Auth.js
@@ -7,10 +7,11 @@ import cache from './cache';
|
||||
// An Auth object tells you who is requesting something and whether
|
||||
// the master key was used.
|
||||
// userObject is a Parse.User and can be null if there's no user.
|
||||
function Auth(config, isMaster, userObject) {
|
||||
function Auth({ config, isMaster = false, user, installationId } = {}) {
|
||||
this.config = config;
|
||||
this.installationId = installationId;
|
||||
this.isMaster = isMaster;
|
||||
this.user = userObject;
|
||||
this.user = user;
|
||||
|
||||
// Assuming a users roles won't change during a single request, we'll
|
||||
// only load them once.
|
||||
@@ -33,19 +34,19 @@ Auth.prototype.couldUpdateUserId = function(userId) {
|
||||
|
||||
// A helper to get a master-level Auth object
|
||||
function master(config) {
|
||||
return new Auth(config, true, null);
|
||||
return new Auth({ config, isMaster: true });
|
||||
}
|
||||
|
||||
// A helper to get a nobody-level Auth object
|
||||
function nobody(config) {
|
||||
return new Auth(config, false, null);
|
||||
return new Auth({ config, isMaster: false });
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to an Auth object
|
||||
var getAuthForSessionToken = function(config, sessionToken) {
|
||||
var cachedUser = cache.getUser(sessionToken);
|
||||
var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) {
|
||||
var cachedUser = cache.users.get(sessionToken);
|
||||
if (cachedUser) {
|
||||
return Promise.resolve(new Auth(config, false, cachedUser));
|
||||
return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser }));
|
||||
}
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
@@ -65,9 +66,9 @@ var getAuthForSessionToken = function(config, sessionToken) {
|
||||
delete obj.password;
|
||||
obj['className'] = '_User';
|
||||
obj['sessionToken'] = sessionToken;
|
||||
var userObject = Parse.Object.fromJSON(obj);
|
||||
cache.setUser(sessionToken, userObject);
|
||||
return new Auth(config, false, userObject);
|
||||
let userObject = Parse.Object.fromJSON(obj);
|
||||
cache.users.set(sessionToken, userObject);
|
||||
return new Auth({ config, isMaster: false, installationId, user: userObject });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -159,6 +160,22 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
var roleIDs = results.map(r => r.objectId);
|
||||
|
||||
var parentRolesPromises = roleIDs.map( (roleId) => {
|
||||
return this._getAllRoleNamesForId(roleId);
|
||||
});
|
||||
parentRolesPromises.push(Promise.resolve(roleIDs));
|
||||
return Promise.all(parentRolesPromises);
|
||||
}).then(function(results){
|
||||
// Flatten
|
||||
let roleIDs = results.reduce( (memo, result) => {
|
||||
if (typeof result == "object") {
|
||||
memo = memo.concat(result);
|
||||
} else {
|
||||
memo.push(result);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
return Promise.resolve(roleIDs);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,15 +7,12 @@ import cache from './cache';
|
||||
export class Config {
|
||||
constructor(applicationId: string, mount: string) {
|
||||
let DatabaseAdapter = require('./DatabaseAdapter');
|
||||
|
||||
let cacheInfo = cache.apps[applicationId];
|
||||
this.valid = !!cacheInfo;
|
||||
if (!this.valid) {
|
||||
let cacheInfo = cache.apps.get(applicationId);
|
||||
if (!cacheInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applicationId = applicationId;
|
||||
this.collectionPrefix = cacheInfo.collectionPrefix || '';
|
||||
this.masterKey = cacheInfo.masterKey;
|
||||
this.clientKey = cacheInfo.clientKey;
|
||||
this.javascriptKey = cacheInfo.javascriptKey;
|
||||
@@ -25,16 +22,64 @@ export class Config {
|
||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
|
||||
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, this.collectionPrefix);
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
|
||||
|
||||
this.serverURL = cacheInfo.serverURL;
|
||||
this.publicServerURL = cacheInfo.publicServerURL;
|
||||
this.verifyUserEmails = cacheInfo.verifyUserEmails;
|
||||
this.appName = cacheInfo.appName;
|
||||
|
||||
this.hooksController = cacheInfo.hooksController;
|
||||
this.filesController = cacheInfo.filesController;
|
||||
this.pushController = cacheInfo.pushController;
|
||||
this.pushController = cacheInfo.pushController;
|
||||
this.loggerController = cacheInfo.loggerController;
|
||||
this.userController = cacheInfo.userController;
|
||||
this.oauth = cacheInfo.oauth;
|
||||
|
||||
this.customPages = cacheInfo.customPages || {};
|
||||
this.mount = mount;
|
||||
}
|
||||
}
|
||||
|
||||
static validate(options) {
|
||||
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
|
||||
appName: options.appName,
|
||||
publicServerURL: options.publicServerURL})
|
||||
}
|
||||
|
||||
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
|
||||
if (verifyUserEmails) {
|
||||
if (typeof appName !== 'string') {
|
||||
throw 'An app name is required when using email verification.';
|
||||
}
|
||||
if (typeof publicServerURL !== 'string') {
|
||||
throw 'A public server url is required when using email verification.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get invalidLinkURL() {
|
||||
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
|
||||
}
|
||||
|
||||
get verifyEmailSuccessURL() {
|
||||
return this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html`;
|
||||
}
|
||||
|
||||
get choosePasswordURL() {
|
||||
return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`;
|
||||
}
|
||||
|
||||
get requestResetPasswordURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`;
|
||||
}
|
||||
|
||||
get passwordResetSuccessURL() {
|
||||
return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`;
|
||||
}
|
||||
|
||||
get verifyEmailURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
|
||||
}
|
||||
};
|
||||
|
||||
export default Config;
|
||||
module.exports = Config;
|
||||
|
||||
@@ -10,13 +10,20 @@ based on the parameters passed
|
||||
|
||||
// _adapter is private, use Symbol
|
||||
var _adapter = Symbol();
|
||||
import Config from '../Config';
|
||||
|
||||
export class AdaptableController {
|
||||
|
||||
constructor(adapter) {
|
||||
constructor(adapter, appId, options) {
|
||||
this.options = options;
|
||||
this.appId = appId;
|
||||
this.adapter = adapter;
|
||||
this.setFeature();
|
||||
}
|
||||
|
||||
// sets features for Dashboard to consume from features router
|
||||
setFeature() {}
|
||||
|
||||
set adapter(adapter) {
|
||||
this.validateAdapter(adapter);
|
||||
this[_adapter] = adapter;
|
||||
@@ -26,12 +33,15 @@ export class AdaptableController {
|
||||
return this[_adapter];
|
||||
}
|
||||
|
||||
get config() {
|
||||
return new Config(this.appId);
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
throw new Error("Subclasses should implement expectedAdapterType()");
|
||||
}
|
||||
|
||||
validateAdapter(adapter) {
|
||||
|
||||
if (!adapter) {
|
||||
throw new Error(this.constructor.name+" requires an adapter");
|
||||
}
|
||||
@@ -56,10 +66,9 @@ export class AdaptableController {
|
||||
}, {});
|
||||
|
||||
if (Object.keys(mismatches).length > 0) {
|
||||
console.error(adapter, mismatches);
|
||||
throw new Error("Adapter prototype don't match expected prototype");
|
||||
throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AdaptableController;
|
||||
export default AdaptableController;
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
// Parse database.
|
||||
|
||||
var mongodb = require('mongodb');
|
||||
var MongoClient = mongodb.MongoClient;
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
var Schema = require('./Schema');
|
||||
var transform = require('./transform');
|
||||
var Schema = require('./../Schema');
|
||||
var transform = require('./../transform');
|
||||
|
||||
// options can contain:
|
||||
// collectionPrefix: the string to put in front of every collection name.
|
||||
function ExportAdapter(mongoURI, options = {}) {
|
||||
this.mongoURI = mongoURI;
|
||||
function DatabaseController(adapter, { collectionPrefix } = {}) {
|
||||
this.adapter = adapter;
|
||||
|
||||
this.collectionPrefix = options.collectionPrefix;
|
||||
this.collectionPrefix = collectionPrefix;
|
||||
|
||||
// We don't want a mutable this.schema, because then you could have
|
||||
// one request that uses different schemas for different parts of
|
||||
@@ -25,25 +24,13 @@ function ExportAdapter(mongoURI, options = {}) {
|
||||
|
||||
// Connects to the database. Returns a promise that resolves when the
|
||||
// connection is successful.
|
||||
// this.db will be populated with a Mongo "Db" object when the
|
||||
// promise resolves successfully.
|
||||
ExportAdapter.prototype.connect = function() {
|
||||
if (this.connectionPromise) {
|
||||
// There's already a connection in progress.
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
this.connectionPromise = Promise.resolve().then(() => {
|
||||
return MongoClient.connect(this.mongoURI);
|
||||
}).then((db) => {
|
||||
this.db = db;
|
||||
});
|
||||
return this.connectionPromise;
|
||||
DatabaseController.prototype.connect = function() {
|
||||
return this.adapter.connect();
|
||||
};
|
||||
|
||||
// Returns a promise for a Mongo collection.
|
||||
// Generally just for internal use.
|
||||
ExportAdapter.prototype.collection = function(className) {
|
||||
DatabaseController.prototype.collection = function(className) {
|
||||
if (!Schema.classNameIsValid(className)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
|
||||
'invalid className: ' + className);
|
||||
@@ -51,10 +38,20 @@ ExportAdapter.prototype.collection = function(className) {
|
||||
return this.rawCollection(className);
|
||||
};
|
||||
|
||||
ExportAdapter.prototype.rawCollection = function(className) {
|
||||
return this.connect().then(() => {
|
||||
return this.db.collection(this.collectionPrefix + className);
|
||||
});
|
||||
DatabaseController.prototype.adaptiveCollection = function(className) {
|
||||
return this.adapter.adaptiveCollection(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.collectionExists = function(className) {
|
||||
return this.adapter.collectionExists(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.rawCollection = function(className) {
|
||||
return this.adapter.collection(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.dropCollection = function(className) {
|
||||
return this.adapter.dropCollection(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
function returnsTrue() {
|
||||
@@ -64,7 +61,7 @@ function returnsTrue() {
|
||||
// Returns a promise for a schema object.
|
||||
// If we are provided a acceptor, then we run it on the schema.
|
||||
// If the schema isn't accepted, we reload it at most once.
|
||||
ExportAdapter.prototype.loadSchema = function(acceptor = returnsTrue) {
|
||||
DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
|
||||
|
||||
if (!this.schemaPromise) {
|
||||
this.schemaPromise = this.collection('_SCHEMA').then((coll) => {
|
||||
@@ -88,8 +85,8 @@ ExportAdapter.prototype.loadSchema = function(acceptor = returnsTrue) {
|
||||
|
||||
// Returns a promise for the classname that is related to the given
|
||||
// classname through the key.
|
||||
// TODO: make this not in the ExportAdapter interface
|
||||
ExportAdapter.prototype.redirectClassNameForKey = function(className, key) {
|
||||
// TODO: make this not in the DatabaseController interface
|
||||
DatabaseController.prototype.redirectClassNameForKey = function(className, key) {
|
||||
return this.loadSchema().then((schema) => {
|
||||
var t = schema.getExpectedType(className, key);
|
||||
var match = t.match(/^relation<(.*)>$/);
|
||||
@@ -105,7 +102,7 @@ ExportAdapter.prototype.redirectClassNameForKey = function(className, key) {
|
||||
// Returns a promise that resolves to the new schema.
|
||||
// This does not update this.schema, because in a situation like a
|
||||
// batch request, that could confuse other users of the schema.
|
||||
ExportAdapter.prototype.validateObject = function(className, object, query) {
|
||||
DatabaseController.prototype.validateObject = function(className, object, query) {
|
||||
return this.loadSchema().then((schema) => {
|
||||
return schema.validateObject(className, object, query);
|
||||
});
|
||||
@@ -113,7 +110,7 @@ ExportAdapter.prototype.validateObject = function(className, object, query) {
|
||||
|
||||
// Like transform.untransformObject but you need to provide a className.
|
||||
// Filters out any data that shouldn't be on this REST-formatted object.
|
||||
ExportAdapter.prototype.untransformObject = function(
|
||||
DatabaseController.prototype.untransformObject = function(
|
||||
schema, isMaster, aclGroup, className, mongoObject) {
|
||||
var object = transform.untransformObject(schema, className, mongoObject);
|
||||
|
||||
@@ -138,65 +135,59 @@ ExportAdapter.prototype.untransformObject = function(
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
ExportAdapter.prototype.update = function(className, query, update, options) {
|
||||
DatabaseController.prototype.update = function(className, query, update, options) {
|
||||
var acceptor = function(schema) {
|
||||
return schema.hasKeys(className, Object.keys(query));
|
||||
};
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
var mongoUpdate, schema;
|
||||
return this.loadSchema(acceptor).then((s) => {
|
||||
schema = s;
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'update');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
|
||||
return this.handleRelationUpdates(className, query.objectId, update);
|
||||
}).then(() => {
|
||||
return this.collection(className);
|
||||
}).then((coll) => {
|
||||
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||
if (options.acl) {
|
||||
var writePerms = [
|
||||
{_wperm: {'$exists': false}}
|
||||
];
|
||||
for (var entry of options.acl) {
|
||||
writePerms.push({_wperm: {'$in': [entry]}});
|
||||
return this.loadSchema(acceptor)
|
||||
.then(s => {
|
||||
schema = s;
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'update');
|
||||
}
|
||||
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||
}
|
||||
|
||||
mongoUpdate = transform.transformUpdate(schema, className, update);
|
||||
|
||||
return coll.findAndModify(mongoWhere, {}, mongoUpdate, {});
|
||||
}).then((result) => {
|
||||
if (!result.value) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'));
|
||||
}
|
||||
if (result.lastErrorObject.n != 1) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'));
|
||||
}
|
||||
|
||||
var response = {};
|
||||
var inc = mongoUpdate['$inc'];
|
||||
if (inc) {
|
||||
for (var key in inc) {
|
||||
response[key] = (result.value[key] || 0) + inc[key];
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => this.handleRelationUpdates(className, query.objectId, update))
|
||||
.then(() => this.adaptiveCollection(className))
|
||||
.then(collection => {
|
||||
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||
if (options.acl) {
|
||||
var writePerms = [
|
||||
{_wperm: {'$exists': false}}
|
||||
];
|
||||
for (var entry of options.acl) {
|
||||
writePerms.push({_wperm: {'$in': [entry]}});
|
||||
}
|
||||
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||
}
|
||||
}
|
||||
return response;
|
||||
});
|
||||
mongoUpdate = transform.transformUpdate(schema, className, update);
|
||||
return collection.findOneAndUpdate(mongoWhere, mongoUpdate);
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'));
|
||||
}
|
||||
|
||||
let response = {};
|
||||
let inc = mongoUpdate['$inc'];
|
||||
if (inc) {
|
||||
Object.keys(inc).forEach(key => {
|
||||
response[key] = result[key];
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
// Processes relation-updating operations from a REST-format update.
|
||||
// Returns a promise that resolves successfully when these are
|
||||
// processed.
|
||||
// This mutates update.
|
||||
ExportAdapter.prototype.handleRelationUpdates = function(className,
|
||||
DatabaseController.prototype.handleRelationUpdates = function(className,
|
||||
objectId,
|
||||
update) {
|
||||
var pending = [];
|
||||
@@ -243,7 +234,7 @@ ExportAdapter.prototype.handleRelationUpdates = function(className,
|
||||
|
||||
// Adds a relation.
|
||||
// Returns a promise that resolves successfully iff the add was successful.
|
||||
ExportAdapter.prototype.addRelation = function(key, fromClassName,
|
||||
DatabaseController.prototype.addRelation = function(key, fromClassName,
|
||||
fromId, toId) {
|
||||
var doc = {
|
||||
relatedId: toId,
|
||||
@@ -258,7 +249,7 @@ ExportAdapter.prototype.addRelation = function(key, fromClassName,
|
||||
// Removes a relation.
|
||||
// Returns a promise that resolves successfully iff the remove was
|
||||
// successful.
|
||||
ExportAdapter.prototype.removeRelation = function(key, fromClassName,
|
||||
DatabaseController.prototype.removeRelation = function(key, fromClassName,
|
||||
fromId, toId) {
|
||||
var doc = {
|
||||
relatedId: toId,
|
||||
@@ -277,7 +268,7 @@ ExportAdapter.prototype.removeRelation = function(key, fromClassName,
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
ExportAdapter.prototype.destroy = function(className, query, options = {}) {
|
||||
DatabaseController.prototype.destroy = function(className, query, options = {}) {
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
|
||||
@@ -320,7 +311,7 @@ ExportAdapter.prototype.destroy = function(className, query, options = {}) {
|
||||
|
||||
// Inserts an object into the database.
|
||||
// Returns a promise that resolves successfully iff the object saved.
|
||||
ExportAdapter.prototype.create = function(className, object, options) {
|
||||
DatabaseController.prototype.create = function(className, object, options) {
|
||||
var schema;
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
@@ -346,28 +337,21 @@ ExportAdapter.prototype.create = function(className, object, options) {
|
||||
// This should only be used for testing - use 'find' for normal code
|
||||
// to avoid Mongo-format dependencies.
|
||||
// Returns a promise that resolves to a list of items.
|
||||
ExportAdapter.prototype.mongoFind = function(className, query, options = {}) {
|
||||
return this.collection(className).then((coll) => {
|
||||
return coll.find(query, options).toArray();
|
||||
});
|
||||
DatabaseController.prototype.mongoFind = function(className, query, options = {}) {
|
||||
return this.adaptiveCollection(className)
|
||||
.then(collection => collection.find(query, options));
|
||||
};
|
||||
|
||||
// Deletes everything in the database matching the current collectionPrefix
|
||||
// Won't delete collections in the system namespace
|
||||
// Returns a promise.
|
||||
ExportAdapter.prototype.deleteEverything = function() {
|
||||
DatabaseController.prototype.deleteEverything = function() {
|
||||
this.schemaPromise = null;
|
||||
|
||||
return this.connect().then(() => {
|
||||
return this.db.collections();
|
||||
}).then((colls) => {
|
||||
var promises = [];
|
||||
for (var coll of colls) {
|
||||
if (!coll.namespace.match(/\.system\./) &&
|
||||
coll.collectionName.indexOf(this.collectionPrefix) === 0) {
|
||||
promises.push(coll.drop());
|
||||
}
|
||||
}
|
||||
return this.adapter.collectionsContaining(this.collectionPrefix).then(collections => {
|
||||
let promises = collections.map(collection => {
|
||||
return collection.drop();
|
||||
});
|
||||
return Promise.all(promises);
|
||||
});
|
||||
};
|
||||
@@ -376,13 +360,11 @@ ExportAdapter.prototype.deleteEverything = function() {
|
||||
function keysForQuery(query) {
|
||||
var sublist = query['$and'] || query['$or'];
|
||||
if (sublist) {
|
||||
var answer = new Set();
|
||||
for (var subquery of sublist) {
|
||||
for (var key of keysForQuery(subquery)) {
|
||||
answer.add(key);
|
||||
}
|
||||
}
|
||||
return answer;
|
||||
let answer = sublist.reduce((memo, subquery) => {
|
||||
return memo.concat(keysForQuery(subquery));
|
||||
}, []);
|
||||
|
||||
return new Set(answer);
|
||||
}
|
||||
|
||||
return new Set(Object.keys(query));
|
||||
@@ -390,59 +372,74 @@ function keysForQuery(query) {
|
||||
|
||||
// Returns a promise for a list of related ids given an owning id.
|
||||
// className here is the owning className.
|
||||
ExportAdapter.prototype.relatedIds = function(className, key, owningId) {
|
||||
var joinTable = '_Join:' + key + ':' + className;
|
||||
return this.collection(joinTable).then((coll) => {
|
||||
return coll.find({owningId: owningId}).toArray();
|
||||
}).then((results) => {
|
||||
return results.map(r => r.relatedId);
|
||||
});
|
||||
DatabaseController.prototype.relatedIds = function(className, key, owningId) {
|
||||
return this.adaptiveCollection(joinTableName(className, key))
|
||||
.then(coll => coll.find({owningId : owningId}))
|
||||
.then(results => results.map(r => r.relatedId));
|
||||
};
|
||||
|
||||
// Returns a promise for a list of owning ids given some related ids.
|
||||
// className here is the owning className.
|
||||
ExportAdapter.prototype.owningIds = function(className, key, relatedIds) {
|
||||
var joinTable = '_Join:' + key + ':' + className;
|
||||
return this.collection(joinTable).then((coll) => {
|
||||
return coll.find({relatedId: {'$in': relatedIds}}).toArray();
|
||||
}).then((results) => {
|
||||
return results.map(r => r.owningId);
|
||||
});
|
||||
DatabaseController.prototype.owningIds = function(className, key, relatedIds) {
|
||||
return this.adaptiveCollection(joinTableName(className, key))
|
||||
.then(coll => coll.find({ relatedId: { '$in': relatedIds } }))
|
||||
.then(results => results.map(r => r.owningId));
|
||||
};
|
||||
|
||||
// Modifies query so that it no longer has $in on relation fields, or
|
||||
// equal-to-pointer constraints on relation fields.
|
||||
// Returns a promise that resolves when query is mutated
|
||||
// TODO: this only handles one of these at a time - make it handle more
|
||||
ExportAdapter.prototype.reduceInRelation = function(className, query, schema) {
|
||||
DatabaseController.prototype.reduceInRelation = function(className, query, schema) {
|
||||
|
||||
// Search for an in-relation or equal-to-relation
|
||||
for (var key in query) {
|
||||
if (query[key] &&
|
||||
(query[key]['$in'] || query[key].__type == 'Pointer')) {
|
||||
var t = schema.getExpectedType(className, key);
|
||||
var match = t ? t.match(/^relation<(.*)>$/) : false;
|
||||
// Make it sequential for now, not sure of paralleization side effects
|
||||
if (query['$or']) {
|
||||
let ors = query['$or'];
|
||||
return Promise.all(ors.map((aQuery, index) => {
|
||||
return this.reduceInRelation(className, aQuery, schema).then((aQuery) => {
|
||||
query['$or'][index] = aQuery;
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
let promises = Object.keys(query).map((key) => {
|
||||
if (query[key] && (query[key]['$in'] || query[key].__type == 'Pointer')) {
|
||||
let t = schema.getExpectedType(className, key);
|
||||
let match = t ? t.match(/^relation<(.*)>$/) : false;
|
||||
if (!match) {
|
||||
continue;
|
||||
return Promise.resolve(query);
|
||||
}
|
||||
var relatedClassName = match[1];
|
||||
var relatedIds;
|
||||
let relatedClassName = match[1];
|
||||
let relatedIds;
|
||||
if (query[key]['$in']) {
|
||||
relatedIds = query[key]['$in'].map(r => r.objectId);
|
||||
} else {
|
||||
relatedIds = [query[key].objectId];
|
||||
}
|
||||
return this.owningIds(className, key, relatedIds).then((ids) => {
|
||||
delete query[key];
|
||||
query.objectId = {'$in': ids};
|
||||
delete query[key];
|
||||
this.addInObjectIdsIds(ids, query);
|
||||
return Promise.resolve(query);
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(query);
|
||||
})
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return Promise.resolve(query);
|
||||
})
|
||||
};
|
||||
|
||||
// Modifies query so that it no longer has $relatedTo
|
||||
// Returns a promise that resolves when query is mutated
|
||||
ExportAdapter.prototype.reduceRelationKeys = function(className, query) {
|
||||
DatabaseController.prototype.reduceRelationKeys = function(className, query) {
|
||||
|
||||
if (query['$or']) {
|
||||
return Promise.all(query['$or'].map((aQuery) => {
|
||||
return this.reduceRelationKeys(className, aQuery);
|
||||
}));
|
||||
}
|
||||
|
||||
var relatedTo = query['$relatedTo'];
|
||||
if (relatedTo) {
|
||||
return this.relatedIds(
|
||||
@@ -450,43 +447,22 @@ ExportAdapter.prototype.reduceRelationKeys = function(className, query) {
|
||||
relatedTo.key,
|
||||
relatedTo.object.objectId).then((ids) => {
|
||||
delete query['$relatedTo'];
|
||||
query['objectId'] = {'$in': ids};
|
||||
this.addInObjectIdsIds(ids, query);
|
||||
return this.reduceRelationKeys(className, query);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Does a find with "smart indexing".
|
||||
// Currently this just means, if it needs a geoindex and there is
|
||||
// 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.
|
||||
ExportAdapter.prototype.smartFind = function(coll, where, options) {
|
||||
return coll.find(where, options).toArray()
|
||||
.then((result) => {
|
||||
return result;
|
||||
}, (error) => {
|
||||
// Check for "no geoindex" error
|
||||
if (!error.message.match(/unable to find index for .geoNear/) ||
|
||||
error.code != 17007) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Figure out what key needs an index
|
||||
var key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
//TODO: condiser moving index creation logic into Schema.js
|
||||
return coll.createIndex(index).then(() => {
|
||||
// Retry, but just once.
|
||||
return coll.find(where, options).toArray();
|
||||
});
|
||||
});
|
||||
};
|
||||
DatabaseController.prototype.addInObjectIdsIds = function(ids, query) {
|
||||
if (typeof query.objectId == 'string') {
|
||||
query.objectId = {'$in': [query.objectId]};
|
||||
}
|
||||
query.objectId = query.objectId || {};
|
||||
let queryIn = [].concat(query.objectId['$in'] || [], ids || []);
|
||||
// make a set and spread to remove duplicates
|
||||
query.objectId = {'$in': [...new Set(queryIn)]};
|
||||
return query;
|
||||
}
|
||||
|
||||
// Runs a query on the database.
|
||||
// Returns a promise that resolves to a list of items.
|
||||
@@ -502,7 +478,7 @@ ExportAdapter.prototype.smartFind = function(coll, where, options) {
|
||||
// TODO: make userIds not needed here. The db adapter shouldn't know
|
||||
// anything about users, ideally. Then, improve the format of the ACL
|
||||
// arg to work like the others.
|
||||
ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||
DatabaseController.prototype.find = function(className, query, options = {}) {
|
||||
var mongoOptions = {};
|
||||
if (options.skip) {
|
||||
mongoOptions.skip = options.skip;
|
||||
@@ -541,8 +517,8 @@ ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||
}).then(() => {
|
||||
return this.reduceInRelation(className, query, schema);
|
||||
}).then(() => {
|
||||
return this.collection(className);
|
||||
}).then((coll) => {
|
||||
return this.adaptiveCollection(className);
|
||||
}).then(collection => {
|
||||
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||
if (!isMaster) {
|
||||
var orParts = [
|
||||
@@ -555,9 +531,10 @@ ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||
mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]};
|
||||
}
|
||||
if (options.count) {
|
||||
return coll.count(mongoWhere, mongoOptions);
|
||||
delete mongoOptions.limit;
|
||||
return collection.count(mongoWhere, mongoOptions);
|
||||
} else {
|
||||
return this.smartFind(coll, mongoWhere, mongoOptions)
|
||||
return collection.find(mongoWhere, mongoOptions)
|
||||
.then((mongoResults) => {
|
||||
return mongoResults.map((r) => {
|
||||
return this.untransformObject(
|
||||
@@ -568,4 +545,8 @@ ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = ExportAdapter;
|
||||
function joinTableName(className, key) {
|
||||
return `_Join:${key}:${className}`;
|
||||
}
|
||||
|
||||
module.exports = DatabaseController;
|
||||
@@ -3,6 +3,8 @@ import { Parse } from 'parse/node';
|
||||
import { randomHexString } from '../cryptoUtils';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
|
||||
import path from 'path';
|
||||
import mime from 'mime';
|
||||
|
||||
export class FilesController extends AdaptableController {
|
||||
|
||||
@@ -10,10 +12,22 @@ export class FilesController extends AdaptableController {
|
||||
return this.adapter.getFileData(config, filename);
|
||||
}
|
||||
|
||||
createFile(config, filename, data) {
|
||||
createFile(config, filename, data, contentType) {
|
||||
|
||||
let extname = path.extname(filename);
|
||||
|
||||
const hasExtension = extname.length > 0;
|
||||
|
||||
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||
filename = filename + '.' + mime.extension(contentType);
|
||||
} else if (hasExtension && !contentType) {
|
||||
contentType = mime.lookup(filename);
|
||||
}
|
||||
|
||||
filename = randomHexString(32) + '_' + filename;
|
||||
|
||||
var location = this.adapter.getFileLocation(config, filename);
|
||||
return this.adapter.createFile(config, filename, data).then(() => {
|
||||
return this.adapter.createFile(config, filename, data, contentType).then(() => {
|
||||
return Promise.resolve({
|
||||
url: location,
|
||||
name: filename
|
||||
|
||||
@@ -3,9 +3,18 @@ import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import { PushAdapter } from '../Adapters/Push/PushAdapter';
|
||||
import deepcopy from 'deepcopy';
|
||||
import features from '../features';
|
||||
|
||||
const FEATURE_NAME = 'push';
|
||||
const UNSUPPORTED_BADGE_KEY = "unsupported";
|
||||
|
||||
export class PushController extends AdaptableController {
|
||||
|
||||
setFeature() {
|
||||
features.setFeature(FEATURE_NAME, this.adapter.feature || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the deviceType parameter in qury condition is valid or not.
|
||||
* @param {Object} where A query condition
|
||||
@@ -51,7 +60,55 @@ export class PushController extends AdaptableController {
|
||||
body['expiration_time'] = PushController.getExpirationTime(body);
|
||||
// TODO: If the req can pass the checking, we return immediately instead of waiting
|
||||
// pushes to be sent. We probably change this behaviour in the future.
|
||||
rest.find(config, auth, '_Installation', where).then(function(response) {
|
||||
let badgeUpdate = Promise.resolve();
|
||||
|
||||
if (body.badge) {
|
||||
var op = {};
|
||||
if (body.badge == "Increment") {
|
||||
op = {'$inc': {'badge': 1}}
|
||||
} else if (Number(body.badge)) {
|
||||
op = {'$set': {'badge': body.badge } }
|
||||
} else {
|
||||
throw "Invalid value for badge, expected number or 'Increment'";
|
||||
}
|
||||
let updateWhere = deepcopy(where);
|
||||
|
||||
// Only on iOS!
|
||||
updateWhere.deviceType = 'ios';
|
||||
|
||||
// TODO: @nlutsenko replace with better thing
|
||||
badgeUpdate = config.database.rawCollection("_Installation").then((coll) => {
|
||||
return coll.update(updateWhere, op, { multi: true });
|
||||
});
|
||||
}
|
||||
|
||||
return badgeUpdate.then(() => {
|
||||
return rest.find(config, auth, '_Installation', where)
|
||||
}).then((response) => {
|
||||
if (body.badge && body.badge == "Increment") {
|
||||
// Collect the badges to reduce the # of calls
|
||||
let badgeInstallationsMap = response.results.reduce((map, installation) => {
|
||||
let badge = installation.badge;
|
||||
if (installation.deviceType != "ios") {
|
||||
badge = UNSUPPORTED_BADGE_KEY;
|
||||
}
|
||||
map[badge] = map[badge] || [];
|
||||
map[badge].push(installation);
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// Map the on the badges count and return the send result
|
||||
let promises = Object.keys(badgeInstallationsMap).map((badge) => {
|
||||
let payload = deepcopy(body);
|
||||
if (badge == UNSUPPORTED_BADGE_KEY) {
|
||||
delete payload.badge;
|
||||
} else {
|
||||
payload.badge = parseInt(badge);
|
||||
}
|
||||
return pushAdapter.send(payload, badgeInstallationsMap[badge]);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
return pushAdapter.send(body, response.results);
|
||||
});
|
||||
}
|
||||
|
||||
203
src/Controllers/UserController.js
Normal file
203
src/Controllers/UserController.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import { randomString } from '../cryptoUtils';
|
||||
import { inflate } from '../triggers';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import MailAdapter from '../Adapters/Email/MailAdapter';
|
||||
|
||||
var DatabaseAdapter = require('../DatabaseAdapter');
|
||||
var RestWrite = require('../RestWrite');
|
||||
var RestQuery = require('../RestQuery');
|
||||
var hash = require('../password').hash;
|
||||
var Auth = require('../Auth');
|
||||
|
||||
export class UserController extends AdaptableController {
|
||||
|
||||
constructor(adapter, appId, options = {}) {
|
||||
super(adapter, appId, options);
|
||||
}
|
||||
|
||||
validateAdapter(adapter) {
|
||||
// Allow no adapter
|
||||
if (!adapter && !this.shouldVerifyEmails) {
|
||||
return;
|
||||
}
|
||||
super.validateAdapter(adapter);
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
return MailAdapter;
|
||||
}
|
||||
|
||||
get shouldVerifyEmails() {
|
||||
return this.options.verifyUserEmails;
|
||||
}
|
||||
|
||||
setEmailVerifyToken(user) {
|
||||
if (this.shouldVerifyEmails) {
|
||||
user._email_verify_token = randomString(25);
|
||||
user.emailVerified = false;
|
||||
}
|
||||
}
|
||||
|
||||
verifyEmail(username, token) {
|
||||
if (!this.shouldVerifyEmails) {
|
||||
// Trying to verify email when not enabled
|
||||
// TODO: Better error here.
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return this.config.database
|
||||
.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
// Need direct database access because verification token is not a parse field
|
||||
return collection.findOneAndUpdate({
|
||||
username: username,
|
||||
_email_verify_token: token
|
||||
}, {$set: {emailVerified: true}});
|
||||
})
|
||||
.then(document => {
|
||||
if (!document) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return document;
|
||||
});
|
||||
}
|
||||
|
||||
checkResetTokenValidity(username, token) {
|
||||
return this.config.database.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
return collection.find({
|
||||
username: username,
|
||||
_perishable_token: token
|
||||
}, { limit: 1 });
|
||||
})
|
||||
.then(results => {
|
||||
if (results.length != 1) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return results[0];
|
||||
});
|
||||
}
|
||||
|
||||
getUserIfNeeded(user) {
|
||||
if (user.username && user.email) {
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
var where = {};
|
||||
if (user.username) {
|
||||
where.username = user.username;
|
||||
}
|
||||
if (user.email) {
|
||||
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) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return result.results[0];
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
sendVerificationEmail(user) {
|
||||
if (!this.shouldVerifyEmails) {
|
||||
return;
|
||||
}
|
||||
// We may need to fetch the user in case of update email
|
||||
this.getUserIfNeeded(user).then((user) => {
|
||||
const token = encodeURIComponent(user._email_verify_token);
|
||||
const username = encodeURIComponent(user.username);
|
||||
let link = `${this.config.verifyEmailURL}?token=${token}&username=${username}`;
|
||||
let 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setPasswordResetToken(email) {
|
||||
let token = randomString(25);
|
||||
return this.config.database
|
||||
.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
// Need direct database access because verification token is not a parse field
|
||||
return collection.findOneAndUpdate(
|
||||
{ email: email}, // query
|
||||
{ $set: { _perishable_token: token } } // update
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
sendPasswordResetEmail(email) {
|
||||
if (!this.adapter) {
|
||||
throw "Trying to send a reset password but no adapter is set";
|
||||
// TODO: No adapter?
|
||||
return;
|
||||
}
|
||||
|
||||
return this.setPasswordResetToken(email).then((user) => {
|
||||
|
||||
const token = encodeURIComponent(user._perishable_token);
|
||||
const username = encodeURIComponent(user.username);
|
||||
let link = `${this.config.requestResetPasswordURL}?token=${token}&username=${username}`
|
||||
|
||||
let options = {
|
||||
appName: this.config.appName,
|
||||
link: link,
|
||||
user: inflate('_User', user),
|
||||
};
|
||||
|
||||
if (this.adapter.sendPasswordResetEmail) {
|
||||
this.adapter.sendPasswordResetEmail(options);
|
||||
} else {
|
||||
this.adapter.sendMail(this.defaultResetPasswordEmail(options));
|
||||
}
|
||||
|
||||
return Promise.resolve(user);
|
||||
});
|
||||
}
|
||||
|
||||
updatePassword(username, token, password, config) {
|
||||
return this.checkResetTokenValidity(username, token).then(() => {
|
||||
return updateUserPassword(username, token, password, this.config);
|
||||
});
|
||||
}
|
||||
|
||||
defaultVerificationEmail({link, user, appName, }) {
|
||||
let text = "Hi,\n\n" +
|
||||
"You are being asked to confirm the e-mail address " + user.email + " with " + appName + "\n\n" +
|
||||
"" +
|
||||
"Click here to confirm it:\n" + link;
|
||||
let to = user.get("email");
|
||||
let subject = 'Please verify your e-mail for ' + appName;
|
||||
return { text, to, subject };
|
||||
}
|
||||
|
||||
defaultResetPasswordEmail({link, user, appName, }) {
|
||||
let text = "Hi,\n\n" +
|
||||
"You requested to reset your password for " + appName + ".\n\n" +
|
||||
"" +
|
||||
"Click here to reset it:\n" + link;
|
||||
let to = user.get("email");
|
||||
let subject = 'Password Reset for ' + appName;
|
||||
return { text, to, subject };
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this private
|
||||
function updateUserPassword(username, token, password, config) {
|
||||
var write = new RestWrite(config, Auth.master(config), '_User', {
|
||||
username: username,
|
||||
_perishable_token: token
|
||||
}, {password: password, _perishable_token: null }, undefined);
|
||||
return write.execute();
|
||||
}
|
||||
|
||||
export default UserController;
|
||||
@@ -13,14 +13,17 @@
|
||||
// * destroy(className, query, options)
|
||||
// * This list is incomplete and the database process is not fully modularized.
|
||||
//
|
||||
// Default is ExportAdapter, which uses mongo.
|
||||
// Default is MongoStorageAdapter.
|
||||
|
||||
var ExportAdapter = require('./ExportAdapter');
|
||||
import DatabaseController from './Controllers/DatabaseController';
|
||||
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
|
||||
var adapter = ExportAdapter;
|
||||
var dbConnections = {};
|
||||
var databaseURI = 'mongodb://localhost:27017/parse';
|
||||
var appDatabaseURIs = {};
|
||||
const DefaultDatabaseURI = 'mongodb://localhost:27017/parse';
|
||||
|
||||
let adapter = MongoStorageAdapter;
|
||||
let dbConnections = {};
|
||||
let databaseURI = DefaultDatabaseURI;
|
||||
let appDatabaseURIs = {};
|
||||
|
||||
function setAdapter(databaseAdapter) {
|
||||
adapter = databaseAdapter;
|
||||
@@ -46,10 +49,11 @@ function getDatabaseConnection(appId: string, collectionPrefix: string) {
|
||||
}
|
||||
|
||||
var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI);
|
||||
dbConnections[appId] = new adapter(dbURI, {
|
||||
|
||||
let storageAdapter = new adapter(dbURI);
|
||||
dbConnections[appId] = new DatabaseController(storageAdapter, {
|
||||
collectionPrefix: collectionPrefix
|
||||
});
|
||||
dbConnections[appId].connect();
|
||||
return dbConnections[appId];
|
||||
}
|
||||
|
||||
@@ -59,5 +63,6 @@ module.exports = {
|
||||
setAdapter: setAdapter,
|
||||
setDatabaseURI: setDatabaseURI,
|
||||
setAppDatabaseURI: setAppDatabaseURI,
|
||||
clearDatabaseURIs: clearDatabaseURIs
|
||||
clearDatabaseURIs: clearDatabaseURIs,
|
||||
defaultDatabaseURI: databaseURI
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// themselves use our routing information, without disturbing express
|
||||
// components that external developers may be modifying.
|
||||
|
||||
import express from 'express';
|
||||
|
||||
export default class PromiseRouter {
|
||||
// Each entry should be an object with:
|
||||
// path: the path to route, in express format
|
||||
@@ -15,8 +17,8 @@ export default class PromiseRouter {
|
||||
// status: optional. the http status code. defaults to 200
|
||||
// response: a json object with the content of the response
|
||||
// location: optional. a location header
|
||||
constructor() {
|
||||
this.routes = [];
|
||||
constructor(routes = []) {
|
||||
this.routes = routes;
|
||||
this.mountRoutes();
|
||||
}
|
||||
|
||||
@@ -47,17 +49,11 @@ export default class PromiseRouter {
|
||||
if (handlers.length > 1) {
|
||||
const length = handlers.length;
|
||||
handler = function(req) {
|
||||
var next = function(i, req, res) {
|
||||
if (i == length) {
|
||||
return res;
|
||||
}
|
||||
let result = handlers[i](req);
|
||||
if (!result || typeof result.then !== "function") {
|
||||
result = Promise.resolve(result);
|
||||
}
|
||||
return result.then((res) => (next(i+1, req, res)));
|
||||
}
|
||||
return next(0, req);
|
||||
return handlers.reduce((promise, handler) => {
|
||||
return promise.then((result) => {
|
||||
return handler(req);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +121,29 @@ export default class PromiseRouter {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expressApp() {
|
||||
var expressApp = express();
|
||||
for (var route of this.routes) {
|
||||
switch(route.method) {
|
||||
case 'POST':
|
||||
expressApp.post(route.path, makeExpressHandler(route.handler));
|
||||
break;
|
||||
case 'GET':
|
||||
expressApp.get(route.path, makeExpressHandler(route.handler));
|
||||
break;
|
||||
case 'PUT':
|
||||
expressApp.put(route.path, makeExpressHandler(route.handler));
|
||||
break;
|
||||
case 'DELETE':
|
||||
expressApp.delete(route.path, makeExpressHandler(route.handler));
|
||||
break;
|
||||
default:
|
||||
throw 'unexpected code branch';
|
||||
}
|
||||
}
|
||||
return expressApp;
|
||||
}
|
||||
}
|
||||
|
||||
// Global flag. Set this to true to log every request and response.
|
||||
@@ -142,15 +161,24 @@ function makeExpressHandler(promiseHandler) {
|
||||
JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
promiseHandler(req).then((result) => {
|
||||
if (!result.response) {
|
||||
console.log('BUG: the handler did not include a "response" field');
|
||||
if (!result.response && !result.location && !result.text) {
|
||||
console.log('BUG: the handler did not include a "response" or a "location" field');
|
||||
throw 'control should not get here';
|
||||
}
|
||||
if (PromiseRouter.verbose) {
|
||||
console.log('response:', JSON.stringify(result.response, null, 2));
|
||||
console.log('response:', JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
var status = result.status || 200;
|
||||
res.status(status);
|
||||
|
||||
if (result.text) {
|
||||
return res.send(result.text);
|
||||
}
|
||||
|
||||
if (result.location && !result.response) {
|
||||
return res.redirect(result.location);
|
||||
}
|
||||
if (result.location) {
|
||||
res.set('Location', result.location);
|
||||
}
|
||||
|
||||
@@ -165,7 +165,9 @@ RestQuery.prototype.redirectClassNameForKey = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestQuery.prototype.validateClientClassCreation = function() {
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster) {
|
||||
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.loadSchema().then((schema) => {
|
||||
return schema.hasClass(this.className)
|
||||
}).then((hasClass) => {
|
||||
@@ -212,7 +214,11 @@ RestQuery.prototype.replaceInQuery = function() {
|
||||
});
|
||||
}
|
||||
delete inQueryObject['$inQuery'];
|
||||
inQueryObject['$in'] = values;
|
||||
if (Array.isArray(inQueryObject['$in'])) {
|
||||
inQueryObject['$in'] = inQueryObject['$in'].concat(values);
|
||||
} else {
|
||||
inQueryObject['$in'] = values;
|
||||
}
|
||||
|
||||
// Recurse to repeat
|
||||
return this.replaceInQuery();
|
||||
@@ -249,7 +255,11 @@ RestQuery.prototype.replaceNotInQuery = function() {
|
||||
});
|
||||
}
|
||||
delete notInQueryObject['$notInQuery'];
|
||||
notInQueryObject['$nin'] = values;
|
||||
if (Array.isArray(notInQueryObject['$nin'])) {
|
||||
notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values);
|
||||
} else {
|
||||
notInQueryObject['$nin'] = values;
|
||||
}
|
||||
|
||||
// Recurse to repeat
|
||||
return this.replaceNotInQuery();
|
||||
@@ -269,11 +279,11 @@ RestQuery.prototype.replaceSelect = function() {
|
||||
|
||||
// The select value must have precisely two keys - query and key
|
||||
var selectValue = selectObject['$select'];
|
||||
// iOS SDK don't send where if not set, let it pass
|
||||
if (!selectValue.query ||
|
||||
!selectValue.key ||
|
||||
typeof selectValue.query !== 'object' ||
|
||||
!selectValue.query.className ||
|
||||
!selectValue.query.where ||
|
||||
Object.keys(selectValue).length !== 2) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $select');
|
||||
@@ -288,7 +298,11 @@ RestQuery.prototype.replaceSelect = function() {
|
||||
values.push(result[selectValue.key]);
|
||||
}
|
||||
delete selectObject['$select'];
|
||||
selectObject['$in'] = values;
|
||||
if (Array.isArray(selectObject['$in'])) {
|
||||
selectObject['$in'] = selectObject['$in'].concat(values);
|
||||
} else {
|
||||
selectObject['$in'] = values;
|
||||
}
|
||||
|
||||
// Keep replacing $select clauses
|
||||
return this.replaceSelect();
|
||||
@@ -327,7 +341,11 @@ RestQuery.prototype.replaceDontSelect = function() {
|
||||
values.push(result[dontSelectValue.key]);
|
||||
}
|
||||
delete dontSelectObject['$dontSelect'];
|
||||
dontSelectObject['$nin'] = values;
|
||||
if (Array.isArray(dontSelectObject['$nin'])) {
|
||||
dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values);
|
||||
} else {
|
||||
dontSelectObject['$nin'] = values;
|
||||
}
|
||||
|
||||
// Keep replacing $dontSelect clauses
|
||||
return this.replaceDontSelect();
|
||||
@@ -507,7 +525,7 @@ function replacePointers(object, path, replace) {
|
||||
}
|
||||
|
||||
if (path.length == 0) {
|
||||
if (object.__type == 'Pointer' && replace[object.objectId]) {
|
||||
if (object.__type == 'Pointer') {
|
||||
return replace[object.objectId];
|
||||
}
|
||||
return object;
|
||||
|
||||
@@ -67,12 +67,12 @@ RestWrite.prototype.execute = function() {
|
||||
return this.handleInstallation();
|
||||
}).then(() => {
|
||||
return this.handleSession();
|
||||
}).then(() => {
|
||||
return this.validateAuthData();
|
||||
}).then(() => {
|
||||
return this.runBeforeTrigger();
|
||||
}).then(() => {
|
||||
return this.setRequiredFieldsIfNeeded();
|
||||
}).then(() => {
|
||||
return this.validateAuthData();
|
||||
}).then(() => {
|
||||
return this.transformUser();
|
||||
}).then(() => {
|
||||
@@ -109,7 +109,9 @@ RestWrite.prototype.getUserAndRoleACL = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestWrite.prototype.validateClientClassCreation = function() {
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster) {
|
||||
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.loadSchema().then((schema) => {
|
||||
return schema.hasClass(this.className)
|
||||
}).then((hasClass) => {
|
||||
@@ -134,6 +136,10 @@ RestWrite.prototype.validateSchema = function() {
|
||||
// Runs any beforeSave triggers against this operation.
|
||||
// Any change leads to our data being mutated.
|
||||
RestWrite.prototype.runBeforeTrigger = function() {
|
||||
if (this.response) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid doing any setup for triggers if there is no 'beforeSave' trigger for this class.
|
||||
if (!triggers.triggerExists(this.className, triggers.Types.beforeSave, this.config.applicationId)) {
|
||||
return Promise.resolve();
|
||||
@@ -459,12 +465,18 @@ RestWrite.prototype.transformUser = function() {
|
||||
'address');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}).then(() => {
|
||||
// We updated the email, send a new validation
|
||||
this.storage['sendVerificationEmail'] = true;
|
||||
this.config.userController.setEmailVerifyToken(this.data);
|
||||
return Promise.resolve();
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// Handles any followup logic
|
||||
RestWrite.prototype.handleFollowup = function() {
|
||||
|
||||
if (this.storage && this.storage['clearSessions']) {
|
||||
var sessionQuery = {
|
||||
user: {
|
||||
@@ -474,9 +486,16 @@ RestWrite.prototype.handleFollowup = function() {
|
||||
}
|
||||
};
|
||||
delete this.storage['clearSessions'];
|
||||
return this.config.database.destroy('_Session', sessionQuery)
|
||||
this.config.database.destroy('_Session', sessionQuery)
|
||||
.then(this.handleFollowup.bind(this));
|
||||
}
|
||||
|
||||
if (this.storage && this.storage['sendVerificationEmail']) {
|
||||
delete this.storage['sendVerificationEmail'];
|
||||
// Fire and forget!
|
||||
this.config.userController.sendVerificationEmail(this.data);
|
||||
this.handleFollowup.bind(this);
|
||||
}
|
||||
};
|
||||
|
||||
// Handles the _Role class specialness.
|
||||
@@ -592,6 +611,9 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
|
||||
var promise = Promise.resolve();
|
||||
|
||||
var idMatch; // Will be a match on either objectId or installationId
|
||||
var deviceTokenMatches = [];
|
||||
|
||||
if (this.query && this.query.objectId) {
|
||||
promise = promise.then(() => {
|
||||
return this.config.database.find('_Installation', {
|
||||
@@ -601,22 +623,22 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found for update.');
|
||||
}
|
||||
var existing = results[0];
|
||||
if (this.data.installationId && existing.installationId &&
|
||||
this.data.installationId !== existing.installationId) {
|
||||
idMatch = results[0];
|
||||
if (this.data.installationId && idMatch.installationId &&
|
||||
this.data.installationId !== idMatch.installationId) {
|
||||
throw new Parse.Error(136,
|
||||
'installationId may not be changed in this ' +
|
||||
'operation');
|
||||
}
|
||||
if (this.data.deviceToken && existing.deviceToken &&
|
||||
this.data.deviceToken !== existing.deviceToken &&
|
||||
!this.data.installationId && !existing.installationId) {
|
||||
if (this.data.deviceToken && idMatch.deviceToken &&
|
||||
this.data.deviceToken !== idMatch.deviceToken &&
|
||||
!this.data.installationId && !idMatch.installationId) {
|
||||
throw new Parse.Error(136,
|
||||
'deviceToken may not be changed in this ' +
|
||||
'operation');
|
||||
}
|
||||
if (this.data.deviceType && this.data.deviceType &&
|
||||
this.data.deviceType !== existing.deviceType) {
|
||||
this.data.deviceType !== idMatch.deviceType) {
|
||||
throw new Parse.Error(136,
|
||||
'deviceType may not be changed in this ' +
|
||||
'operation');
|
||||
@@ -627,8 +649,6 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
}
|
||||
|
||||
// Check if we already have installations for the installationId/deviceToken
|
||||
var installationMatch;
|
||||
var deviceTokenMatches = [];
|
||||
promise = promise.then(() => {
|
||||
if (this.data.installationId) {
|
||||
return this.config.database.find('_Installation', {
|
||||
@@ -639,7 +659,7 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
}).then((results) => {
|
||||
if (results && results.length) {
|
||||
// We only take the first match by installationId
|
||||
installationMatch = results[0];
|
||||
idMatch = results[0];
|
||||
}
|
||||
if (this.data.deviceToken) {
|
||||
return this.config.database.find(
|
||||
@@ -651,7 +671,7 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
if (results) {
|
||||
deviceTokenMatches = results;
|
||||
}
|
||||
if (!installationMatch) {
|
||||
if (!idMatch) {
|
||||
if (!deviceTokenMatches.length) {
|
||||
return;
|
||||
} else if (deviceTokenMatches.length == 1 &&
|
||||
@@ -689,14 +709,14 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
// Exactly one device token match and it doesn't have an installation
|
||||
// ID. This is the one case where we want to merge with the existing
|
||||
// object.
|
||||
var delQuery = {objectId: installationMatch.objectId};
|
||||
var delQuery = {objectId: idMatch.objectId};
|
||||
return this.config.database.destroy('_Installation', delQuery)
|
||||
.then(() => {
|
||||
return deviceTokenMatches[0]['objectId'];
|
||||
});
|
||||
} else {
|
||||
if (this.data.deviceToken &&
|
||||
installationMatch.deviceToken != this.data.deviceToken) {
|
||||
idMatch.deviceToken != this.data.deviceToken) {
|
||||
// We're setting the device token on an existing installation, so
|
||||
// we should try cleaning out old installations that match this
|
||||
// device token.
|
||||
@@ -712,7 +732,7 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
this.config.database.destroy('_Installation', delQuery);
|
||||
}
|
||||
// In non-merge scenarios, just return the installation match id
|
||||
return installationMatch.objectId;
|
||||
return idMatch.objectId;
|
||||
}
|
||||
}
|
||||
}).then((objId) => {
|
||||
@@ -762,8 +782,10 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
// Run an update
|
||||
return this.config.database.update(
|
||||
this.className, this.query, this.data, this.runOptions).then((resp) => {
|
||||
this.response = resp;
|
||||
this.response.updatedAt = this.updatedAt;
|
||||
resp.updatedAt = this.updatedAt;
|
||||
this.response = {
|
||||
response: resp
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Set the default ACL for the new _User
|
||||
@@ -794,22 +816,33 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
|
||||
// Returns nothing - doesn't wait for the trigger.
|
||||
RestWrite.prototype.runAfterTrigger = function() {
|
||||
if (!this.response || !this.response.response) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid doing any setup for triggers if there is no 'afterSave' trigger for this class.
|
||||
if (!triggers.triggerExists(this.className, triggers.Types.afterSave, this.config.applicationId)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var extraData = {className: this.className};
|
||||
if (this.query && this.query.objectId) {
|
||||
extraData.objectId = this.query.objectId;
|
||||
}
|
||||
|
||||
// Build the inflated object, different from beforeSave, originalData is not empty
|
||||
// since developers can change data in the beforeSave.
|
||||
var inflatedObject = triggers.inflate(extraData, this.originalData);
|
||||
inflatedObject._finishFetch(this.data);
|
||||
// Build the original object, we only do this for a update write.
|
||||
var originalObject;
|
||||
let originalObject;
|
||||
if (this.query && this.query.objectId) {
|
||||
originalObject = triggers.inflate(extraData, this.originalData);
|
||||
}
|
||||
|
||||
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, inflatedObject, originalObject, this.config.applicationId);
|
||||
// Build the inflated object, different from beforeSave, originalData is not empty
|
||||
// since developers can change data in the beforeSave.
|
||||
let updatedObject = triggers.inflate(extraData, this.originalData);
|
||||
updatedObject.set(Parse._decode(undefined, this.data));
|
||||
updatedObject._handleSaveResponse(this.response.response, this.response.status || 200);
|
||||
|
||||
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config.applicationId);
|
||||
};
|
||||
|
||||
// A helper to figure out what location this operation happens at.
|
||||
@@ -825,4 +858,5 @@ RestWrite.prototype.objectId = function() {
|
||||
return this.data.objectId || this.query.objectId;
|
||||
};
|
||||
|
||||
export default RestWrite;
|
||||
module.exports = RestWrite;
|
||||
|
||||
@@ -85,10 +85,7 @@ export class ClassesRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
handleUpdate(req) {
|
||||
return rest.update(req.config, req.auth, req.params.className, req.params.objectId, req.body)
|
||||
.then((response) => {
|
||||
return {response: response};
|
||||
});
|
||||
return rest.update(req.config, req.auth, req.params.className, req.params.objectId, req.body);
|
||||
}
|
||||
|
||||
handleDelete(req) {
|
||||
|
||||
15
src/Routers/FeaturesRouter.js
Normal file
15
src/Routers/FeaturesRouter.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { version } from '../../package.json';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
import { getFeatures } from '../features';
|
||||
|
||||
export class FeaturesRouter extends PromiseRouter {
|
||||
mountRoutes() {
|
||||
this.route('GET','/serverInfo', middleware.promiseEnforceMasterKeyAccess, () => {
|
||||
return { response: {
|
||||
features: getFeatures(),
|
||||
parseServerVersion: version,
|
||||
} };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import express from 'express';
|
||||
import BodyParser from 'body-parser';
|
||||
import * as Middlewares from '../middlewares';
|
||||
import { randomHexString } from '../cryptoUtils';
|
||||
import mime from 'mime';
|
||||
import Config from '../Config';
|
||||
import mime from 'mime';
|
||||
|
||||
export class FilesRouter {
|
||||
|
||||
@@ -41,7 +41,7 @@ export class FilesRouter {
|
||||
var contentType = mime.lookup(filename);
|
||||
res.set('Content-Type', contentType);
|
||||
res.end(data);
|
||||
}).catch(() => {
|
||||
}).catch((err) => {
|
||||
res.status(404);
|
||||
res.set('Content-Type', 'text/plain');
|
||||
res.end('File not found.');
|
||||
@@ -66,20 +66,13 @@ export class FilesRouter {
|
||||
'Filename contains invalid characters.'));
|
||||
return;
|
||||
}
|
||||
let extension = '';
|
||||
|
||||
// Not very safe there.
|
||||
const hasExtension = req.params.filename.indexOf('.') > 0;
|
||||
const filename = req.params.filename;
|
||||
const contentType = req.get('Content-type');
|
||||
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||
extension = '.' + mime.extension(contentType);
|
||||
}
|
||||
|
||||
const filename = req.params.filename + extension;
|
||||
const config = req.config;
|
||||
const filesController = config.filesController;
|
||||
|
||||
filesController.createFile(config, filename, req.body).then((result) => {
|
||||
filesController.createFile(config, filename, req.body, contentType).then((result) => {
|
||||
res.status(201);
|
||||
res.set('Location', result.url);
|
||||
res.json(result);
|
||||
|
||||
42
src/Routers/GlobalConfigRouter.js
Normal file
42
src/Routers/GlobalConfigRouter.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// global_config.js
|
||||
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
export class GlobalConfigRouter extends PromiseRouter {
|
||||
getGlobalConfig(req) {
|
||||
return req.config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.findOne({'_id': 1}))
|
||||
.then(globalConfig => ({response: { params: globalConfig.params }}))
|
||||
.catch(() => ({
|
||||
status: 404,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'config does not exist',
|
||||
}
|
||||
}));
|
||||
}
|
||||
updateGlobalConfig(req) {
|
||||
return req.config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body }))
|
||||
.then(response => {
|
||||
return { response: { result: true } }
|
||||
})
|
||||
.catch(() => ({
|
||||
status: 404,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'config cannot be updated',
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/config', req => { return this.getGlobalConfig(req) });
|
||||
this.route('PUT', '/config', middleware.promiseEnforceMasterKeyAccess, req => { return this.updateGlobalConfig(req) });
|
||||
}
|
||||
}
|
||||
|
||||
export default GlobalConfigRouter;
|
||||
@@ -1,15 +1,9 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import { HooksController } from '../Controllers/HooksController';
|
||||
|
||||
function enforceMasterKeyAccess(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
throw new Parse.Error(403, "unauthorized: master key is required");
|
||||
}
|
||||
}
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
export class HooksRouter extends PromiseRouter {
|
||||
|
||||
createHook(aHook, config) {
|
||||
return config.hooksController.createHook(aHook).then( (hook) => ({response: hook}));
|
||||
};
|
||||
@@ -93,14 +87,14 @@ export class HooksRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/hooks/functions', enforceMasterKeyAccess, this.handleGetFunctions.bind(this));
|
||||
this.route('GET', '/hooks/triggers', enforceMasterKeyAccess, this.handleGetTriggers.bind(this));
|
||||
this.route('GET', '/hooks/functions/:functionName', enforceMasterKeyAccess, this.handleGetFunctions.bind(this));
|
||||
this.route('GET', '/hooks/triggers/:className/:triggerName', enforceMasterKeyAccess, this.handleGetTriggers.bind(this));
|
||||
this.route('POST', '/hooks/functions', enforceMasterKeyAccess, this.handlePost.bind(this));
|
||||
this.route('POST', '/hooks/triggers', enforceMasterKeyAccess, this.handlePost.bind(this));
|
||||
this.route('PUT', '/hooks/functions/:functionName', enforceMasterKeyAccess, this.handlePut.bind(this));
|
||||
this.route('PUT', '/hooks/triggers/:className/:triggerName', enforceMasterKeyAccess, this.handlePut.bind(this));
|
||||
this.route('GET', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this));
|
||||
this.route('GET', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this));
|
||||
this.route('GET', '/hooks/functions/:functionName', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this));
|
||||
this.route('GET', '/hooks/triggers/:className/:triggerName', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this));
|
||||
this.route('POST', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handlePost.bind(this));
|
||||
this.route('POST', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handlePost.bind(this));
|
||||
this.route('PUT', '/hooks/functions/:functionName', middleware.promiseEnforceMasterKeyAccess, this.handlePut.bind(this));
|
||||
this.route('PUT', '/hooks/triggers/:className/:triggerName', middleware.promiseEnforceMasterKeyAccess, this.handlePut.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
|
||||
// only allow request with master key
|
||||
let enforceSecurity = (auth) => {
|
||||
if (!auth || !auth.isMaster) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Clients aren\'t allowed to perform the ' +
|
||||
'get' + ' operation on logs.'
|
||||
);
|
||||
}
|
||||
}
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
export class LogsRouter extends PromiseRouter {
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET','/logs', (req) => {
|
||||
this.route('GET','/scriptlog', middleware.promiseEnforceMasterKeyAccess, this.validateRequest, (req) => {
|
||||
return this.handleGET(req);
|
||||
});
|
||||
}
|
||||
|
||||
validateRequest(req) {
|
||||
if (!req.config || !req.config.loggerController) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Logger adapter is not availabe');
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise for a {response} object.
|
||||
// query params:
|
||||
// level (optional) Level of logging you want to query for (info || error)
|
||||
@@ -27,28 +24,25 @@ export class LogsRouter extends PromiseRouter {
|
||||
// until (optional) End time for the search. Defaults to current time.
|
||||
// order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
|
||||
// size (optional) Number of rows returned by search. Defaults to 10
|
||||
// n same as size, overrides size if set
|
||||
handleGET(req) {
|
||||
if (!req.config || !req.config.loggerController) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Logger adapter is not availabe');
|
||||
}
|
||||
|
||||
let promise = new Parse.Promise();
|
||||
let from = req.query.from;
|
||||
let until = req.query.until;
|
||||
const from = req.query.from;
|
||||
const until = req.query.until;
|
||||
let size = req.query.size;
|
||||
let order = req.query.order
|
||||
let level = req.query.level;
|
||||
enforceSecurity(req.auth);
|
||||
if (req.query.n) {
|
||||
size = req.query.n;
|
||||
}
|
||||
|
||||
const order = req.query.order
|
||||
const level = req.query.level;
|
||||
const options = {
|
||||
from,
|
||||
until,
|
||||
size,
|
||||
order,
|
||||
level,
|
||||
}
|
||||
|
||||
level
|
||||
};
|
||||
|
||||
return req.config.loggerController.getLogs(options).then((result) => {
|
||||
return Promise.resolve({
|
||||
response: result
|
||||
|
||||
159
src/Routers/PublicAPIRouter.js
Normal file
159
src/Routers/PublicAPIRouter.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import UserController from '../Controllers/UserController';
|
||||
import Config from '../Config';
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
let public_html = path.resolve(__dirname, "../../public_html");
|
||||
let views = path.resolve(__dirname, '../../views');
|
||||
|
||||
export class PublicAPIRouter extends PromiseRouter {
|
||||
|
||||
verifyEmail(req) {
|
||||
let { token, username }= req.query;
|
||||
let appId = req.params.appId;
|
||||
let config = new Config(appId);
|
||||
|
||||
if (!config.publicServerURL) {
|
||||
return this.missingPublicServerURL();
|
||||
}
|
||||
|
||||
if (!token || !username) {
|
||||
return this.invalidLink(req);
|
||||
}
|
||||
|
||||
let userController = config.userController;
|
||||
return userController.verifyEmail(username, token).then( () => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.verifyEmailSuccessURL}?username=${username}`
|
||||
});
|
||||
}, ()=> {
|
||||
return this.invalidLink(req);
|
||||
})
|
||||
}
|
||||
|
||||
changePassword(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let config = new Config(req.query.id);
|
||||
if (!config.publicServerURL) {
|
||||
return resolve({
|
||||
status: 404,
|
||||
text: 'Not found.'
|
||||
});
|
||||
}
|
||||
// Should we keep the file in memory or leave like that?
|
||||
fs.readFile(path.resolve(views, "choose_password"), 'utf-8', (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
data = data.replace("PARSE_SERVER_URL", `'${config.publicServerURL}'`);
|
||||
resolve({
|
||||
text: data
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
requestResetPassword(req) {
|
||||
|
||||
let config = req.config;
|
||||
|
||||
if (!config.publicServerURL) {
|
||||
return this.missingPublicServerURL();
|
||||
}
|
||||
|
||||
let { username, token } = req.query;
|
||||
|
||||
if (!username || !token) {
|
||||
return this.invalidLink(req);
|
||||
}
|
||||
|
||||
return config.userController.checkResetTokenValidity(username, token).then( (user) => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.choosePasswordURL}?token=${token}&id=${config.applicationId}&username=${username}&app=${config.appName}`
|
||||
})
|
||||
}, () => {
|
||||
return this.invalidLink(req);
|
||||
})
|
||||
}
|
||||
|
||||
resetPassword(req) {
|
||||
|
||||
let config = req.config;
|
||||
|
||||
if (!config.publicServerURL) {
|
||||
return this.missingPublicServerURL();
|
||||
}
|
||||
|
||||
let {
|
||||
username,
|
||||
token,
|
||||
new_password
|
||||
} = req.body;
|
||||
|
||||
if (!username || !token || !new_password) {
|
||||
return this.invalidLink(req);
|
||||
}
|
||||
|
||||
return config.userController.updatePassword(username, token, new_password).then((result) => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: config.passwordResetSuccessURL
|
||||
});
|
||||
}, (err) => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.choosePasswordURL}?token=${token}&id=${config.applicationId}&username=${username}&error=${err}&app=${config.appName}`
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
invalidLink(req) {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: req.config.invalidLinkURL
|
||||
});
|
||||
}
|
||||
|
||||
missingPublicServerURL() {
|
||||
return Promise.resolve({
|
||||
text: 'Not found.',
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
setConfig(req) {
|
||||
req.config = new Config(req.params.appId);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET','/apps/:appId/verify_email',
|
||||
req => { this.setConfig(req) },
|
||||
req => { return this.verifyEmail(req); });
|
||||
|
||||
this.route('GET','/apps/choose_password',
|
||||
req => { return this.changePassword(req); });
|
||||
|
||||
this.route('POST','/apps/:appId/request_password_reset',
|
||||
req => { this.setConfig(req) },
|
||||
req => { return this.resetPassword(req); });
|
||||
|
||||
this.route('GET','/apps/:appId/request_password_reset',
|
||||
req => { this.setConfig(req) },
|
||||
req => { return this.requestResetPassword(req); });
|
||||
}
|
||||
|
||||
expressApp() {
|
||||
let router = express();
|
||||
router.use("/apps", express.static(public_html));
|
||||
router.use("/", super.expressApp());
|
||||
return router;
|
||||
}
|
||||
}
|
||||
|
||||
export default PublicAPIRouter;
|
||||
@@ -1,27 +1,17 @@
|
||||
// schemas.js
|
||||
|
||||
var express = require('express'),
|
||||
Parse = require('parse/node').Parse,
|
||||
Schema = require('../Schema');
|
||||
Parse = require('parse/node').Parse,
|
||||
Schema = require('../Schema');
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
|
||||
// TODO: refactor in a SchemaController at one point...
|
||||
function masterKeyRequiredResponse() {
|
||||
return Promise.resolve({
|
||||
status: 401,
|
||||
response: {error: 'master key not specified'},
|
||||
})
|
||||
}
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
function classNameMismatchResponse(bodyClass, pathClass) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class name mismatch between ' + bodyClass + ' and ' + pathClass,
|
||||
}
|
||||
});
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_CLASS_NAME,
|
||||
`Class name mismatch between ${bodyClass} and ${pathClass}.`
|
||||
);
|
||||
}
|
||||
|
||||
function mongoSchemaAPIResponseFields(schema) {
|
||||
@@ -45,65 +35,43 @@ function mongoSchemaToSchemaAPIResponse(schema) {
|
||||
}
|
||||
|
||||
function getAllSchemas(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
return req.config.database.collection('_SCHEMA')
|
||||
.then(coll => coll.find({}).toArray())
|
||||
.then(schemas => ({response: {
|
||||
results: schemas.map(mongoSchemaToSchemaAPIResponse)
|
||||
}}));
|
||||
return req.config.database.adaptiveCollection('_SCHEMA')
|
||||
.then(collection => collection.find({}))
|
||||
.then(schemas => schemas.map(mongoSchemaToSchemaAPIResponse))
|
||||
.then(schemas => ({ response: { results: schemas }}));
|
||||
}
|
||||
|
||||
function getOneSchema(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
return req.config.database.collection('_SCHEMA')
|
||||
.then(coll => coll.findOne({'_id': req.params.className}))
|
||||
.then(schema => ({response: mongoSchemaToSchemaAPIResponse(schema)}))
|
||||
.catch(() => ({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 103,
|
||||
error: 'class ' + req.params.className + ' does not exist',
|
||||
}
|
||||
}));
|
||||
const className = req.params.className;
|
||||
return req.config.database.adaptiveCollection('_SCHEMA')
|
||||
.then(collection => collection.find({ '_id': className }, { limit: 1 }))
|
||||
.then(results => {
|
||||
if (results.length != 1) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
}
|
||||
return results[0];
|
||||
})
|
||||
.then(schema => ({ response: mongoSchemaToSchemaAPIResponse(schema) }));
|
||||
}
|
||||
|
||||
function createSchema(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
if (req.params.className && req.body.className) {
|
||||
if (req.params.className != req.body.className) {
|
||||
return classNameMismatchResponse(req.body.className, req.params.className);
|
||||
}
|
||||
}
|
||||
var className = req.params.className || req.body.className;
|
||||
|
||||
const className = req.params.className || req.body.className;
|
||||
if (!className) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 135,
|
||||
error: 'POST ' + req.path + ' needs class name',
|
||||
},
|
||||
});
|
||||
throw new Parse.Error(135, `POST ${req.path} needs a class name.`);
|
||||
}
|
||||
|
||||
return req.config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists(className, req.body.fields))
|
||||
.then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) }))
|
||||
.catch(error => ({
|
||||
status: 400,
|
||||
response: error,
|
||||
}));
|
||||
.then(schema => schema.addClassIfNotExists(className, req.body.fields))
|
||||
.then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) }));
|
||||
}
|
||||
|
||||
function modifySchema(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
|
||||
if (req.body.className && req.body.className != req.params.className) {
|
||||
return classNameMismatchResponse(req.body.className, req.params.className);
|
||||
}
|
||||
@@ -112,168 +80,115 @@ function modifySchema(req) {
|
||||
var className = req.params.className;
|
||||
|
||||
return req.config.database.loadSchema()
|
||||
.then(schema => {
|
||||
if (!schema.data[className]) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + req.params.className + ' does not exist',
|
||||
.then(schema => {
|
||||
if (!schema.data[className]) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`);
|
||||
}
|
||||
|
||||
let existingFields = schema.data[className];
|
||||
Object.keys(submittedFields).forEach(name => {
|
||||
let field = submittedFields[name];
|
||||
if (existingFields[name] && field.__op !== 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
|
||||
}
|
||||
if (!existingFields[name] && field.__op === 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
var existingFields = schema.data[className];
|
||||
|
||||
for (var submittedFieldName in submittedFields) {
|
||||
if (existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op !== 'Delete') {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 255,
|
||||
error: 'field ' + submittedFieldName + ' exists, cannot update',
|
||||
}
|
||||
});
|
||||
let newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields);
|
||||
let mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className);
|
||||
if (!mongoObject.result) {
|
||||
throw new Parse.Error(mongoObject.code, mongoObject.error);
|
||||
}
|
||||
|
||||
if (!existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op === 'Delete') {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 255,
|
||||
error: 'field ' + submittedFieldName + ' does not exist, cannot delete',
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields);
|
||||
var mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className);
|
||||
if (!mongoObject.result) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: mongoObject,
|
||||
// Finally we have checked to make sure the request is valid and we can start deleting fields.
|
||||
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
||||
let deletionPromises = [];
|
||||
Object.keys(submittedFields).forEach(submittedFieldName => {
|
||||
if (submittedFields[submittedFieldName].__op === 'Delete') {
|
||||
let promise = schema.deleteField(submittedFieldName, className, req.config.database);
|
||||
deletionPromises.push(promise);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Finally we have checked to make sure the request is valid and we can start deleting fields.
|
||||
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
||||
var deletionPromises = []
|
||||
Object.keys(submittedFields).forEach(submittedFieldName => {
|
||||
if (submittedFields[submittedFieldName].__op === 'Delete') {
|
||||
var promise = req.config.database.connect()
|
||||
.then(() => schema.deleteField(
|
||||
submittedFieldName,
|
||||
className,
|
||||
req.config.database.db,
|
||||
req.config.database.collectionPrefix
|
||||
));
|
||||
deletionPromises.push(promise);
|
||||
}
|
||||
return Promise.all(deletionPromises)
|
||||
.then(() => new Promise((resolve, reject) => {
|
||||
schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)});
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(deletionPromises)
|
||||
.then(() => new Promise((resolve, reject) => {
|
||||
schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)});
|
||||
})
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// A helper function that removes all join tables for a schema. Returns a promise.
|
||||
var removeJoinTables = (database, prefix, mongoSchema) => {
|
||||
var removeJoinTables = (database, mongoSchema) => {
|
||||
return Promise.all(Object.keys(mongoSchema)
|
||||
.filter(field => mongoSchema[field].startsWith('relation<'))
|
||||
.map(field => {
|
||||
var joinCollectionName = prefix + '_Join:' + field + ':' + mongoSchema._id;
|
||||
return new Promise((resolve, reject) => {
|
||||
database.dropCollection(joinCollectionName, (err, results) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
});
|
||||
let collectionName = `_Join:${field}:${mongoSchema._id}`;
|
||||
return database.dropCollection(collectionName);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
function deleteSchema(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
|
||||
if (!Schema.classNameIsValid(req.params.className)) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: Schema.invalidClassNameMessage(req.params.className),
|
||||
}
|
||||
});
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, Schema.invalidClassNameMessage(req.params.className));
|
||||
}
|
||||
|
||||
return req.config.database.collection(req.params.className)
|
||||
.then(coll => new Promise((resolve, reject) => {
|
||||
coll.count((err, count) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (count > 0) {
|
||||
resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 255,
|
||||
error: 'class ' + req.params.className + ' not empty, contains ' + count + ' objects, cannot drop schema',
|
||||
}
|
||||
});
|
||||
} else {
|
||||
coll.drop((err, reply) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
// We've dropped the collection now, so delete the item from _SCHEMA
|
||||
// and clear the _Join collections
|
||||
req.config.database.collection('_SCHEMA')
|
||||
.then(coll => new Promise((resolve, reject) => {
|
||||
coll.findAndRemove({ _id: req.params.className }, [], (err, doc) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (doc.value === null) {
|
||||
//tried to delete non-existant class
|
||||
resolve({ response: {}});
|
||||
} else {
|
||||
removeJoinTables(req.config.database.db, req.config.database.collectionPrefix, doc.value)
|
||||
.then(resolve, reject);
|
||||
}
|
||||
});
|
||||
}))
|
||||
.then(resolve.bind(undefined, {response: {}}), reject);
|
||||
}
|
||||
});
|
||||
return req.config.database.collectionExists(req.params.className)
|
||||
.then(exist => {
|
||||
if (!exist) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return req.config.database.adaptiveCollection(req.params.className)
|
||||
.then(collection => {
|
||||
return collection.count()
|
||||
.then(count => {
|
||||
if (count > 0) {
|
||||
throw new Parse.Error(255, `Class ${req.params.className} is not empty, contains ${count} objects, cannot drop schema.`);
|
||||
}
|
||||
return collection.drop();
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
// We've dropped the collection now, so delete the item from _SCHEMA
|
||||
// and clear the _Join collections
|
||||
return req.config.database.adaptiveCollection('_SCHEMA')
|
||||
.then(coll => coll.findOneAndDelete({_id: req.params.className}))
|
||||
.then(document => {
|
||||
if (document === null) {
|
||||
//tried to delete non-existent class
|
||||
return Promise.resolve();
|
||||
}
|
||||
return removeJoinTables(req.config.database, document);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Success
|
||||
return { response: {} };
|
||||
}, error => {
|
||||
if (error.message == 'ns not found') {
|
||||
// If they try to delete a non-existent class, that's fine, just let them.
|
||||
return { response: {} };
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}))
|
||||
.catch( (error) => {
|
||||
if (error.message == 'ns not found') {
|
||||
// If they try to delete a non-existant class, thats fine, just let them.
|
||||
return Promise.resolve({ response: {} });
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
export class SchemasRouter extends PromiseRouter {
|
||||
mountRoutes() {
|
||||
this.route('GET', '/schemas', getAllSchemas);
|
||||
this.route('GET', '/schemas/:className', getOneSchema);
|
||||
this.route('POST', '/schemas', createSchema);
|
||||
this.route('POST', '/schemas/:className', createSchema);
|
||||
this.route('PUT', '/schemas/:className', modifySchema);
|
||||
this.route('DELETE', '/schemas/:className', deleteSchema);
|
||||
this.route('GET', '/schemas', middleware.promiseEnforceMasterKeyAccess, getAllSchemas);
|
||||
this.route('GET', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, getOneSchema);
|
||||
this.route('POST', '/schemas', middleware.promiseEnforceMasterKeyAccess, createSchema);
|
||||
this.route('POST', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, createSchema);
|
||||
this.route('PUT', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, modifySchema);
|
||||
this.route('DELETE', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, deleteSchema);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// These methods handle the User-related routes.
|
||||
|
||||
import deepcopy from 'deepcopy';
|
||||
import deepcopy from 'deepcopy';
|
||||
|
||||
import ClassesRouter from './ClassesRouter';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import Auth from '../Auth';
|
||||
import ClassesRouter from './ClassesRouter';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import Auth from '../Auth';
|
||||
import passwordCrypto from '../password';
|
||||
import RestWrite from '../RestWrite';
|
||||
import { newToken } from '../cryptoUtils';
|
||||
import RestWrite from '../RestWrite';
|
||||
let cryptoUtils = require('../cryptoUtils');
|
||||
let triggers = require('../triggers');
|
||||
|
||||
export class UsersRouter extends ClassesRouter {
|
||||
handleFind(req) {
|
||||
@@ -25,7 +26,18 @@ export class UsersRouter extends ClassesRouter {
|
||||
let data = deepcopy(req.body);
|
||||
req.body = data;
|
||||
req.params.className = '_User';
|
||||
|
||||
//req.config.userController.setEmailVerifyToken(req.body);
|
||||
|
||||
return super.handleCreate(req);
|
||||
|
||||
// if (req.config.verifyUserEmails) {
|
||||
// // Send email as fire-and-forget once the user makes it into the DB.
|
||||
// p.then(() => {
|
||||
// req.config.userController.sendVerificationEmail(req.body);
|
||||
// });
|
||||
// }
|
||||
// return p;
|
||||
}
|
||||
|
||||
handleUpdate(req) {
|
||||
@@ -75,7 +87,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
}
|
||||
|
||||
let user;
|
||||
return req.database.find('_User', { username: req.body.username })
|
||||
return req.config.database.find('_User', { username: req.body.username })
|
||||
.then((results) => {
|
||||
if (!results.length) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||
@@ -87,7 +99,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||
}
|
||||
|
||||
let token = 'r:' + newToken();
|
||||
let token = 'r:' + cryptoUtils.newToken();
|
||||
user.sessionToken = token;
|
||||
delete user.password;
|
||||
|
||||
@@ -140,6 +152,23 @@ export class UsersRouter extends ClassesRouter {
|
||||
}
|
||||
return Promise.resolve(success);
|
||||
}
|
||||
|
||||
handleResetRequest(req) {
|
||||
let { email } = req.body;
|
||||
if (!email) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email");
|
||||
}
|
||||
let userController = req.config.userController;
|
||||
|
||||
return userController.sendPasswordResetEmail(email).then((token) => {
|
||||
return Promise.resolve({
|
||||
response: {}
|
||||
});
|
||||
}, (err) => {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `no user found with email ${email}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/users', req => { return this.handleFind(req); });
|
||||
@@ -150,9 +179,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
|
||||
this.route('GET', '/login', req => { return this.handleLogIn(req); });
|
||||
this.route('POST', '/logout', req => { return this.handleLogOut(req); });
|
||||
this.route('POST', '/requestPasswordReset', () => {
|
||||
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'This path is not implemented yet.');
|
||||
});
|
||||
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
178
src/Schema.js
178
src/Schema.js
@@ -10,7 +10,7 @@
|
||||
// keeping it this way for now.
|
||||
//
|
||||
// In API-handling code, you should only use the Schema class via the
|
||||
// ExportAdapter. This will let us replace the schema logic for
|
||||
// DatabaseController. This will let us replace the schema logic for
|
||||
// different databases.
|
||||
// TODO: hide all schema logic inside the database adapter.
|
||||
|
||||
@@ -48,13 +48,13 @@ var defaultColumns = {
|
||||
// The additional default columns for the _User collection (in addition to DefaultCols)
|
||||
_Role: {
|
||||
"name": {type:'String'},
|
||||
"users": {type:'Relation',className:'_User'},
|
||||
"roles": {type:'Relation',className:'_Role'}
|
||||
"users": {type:'Relation', targetClass:'_User'},
|
||||
"roles": {type:'Relation', targetClass:'_Role'}
|
||||
},
|
||||
// The additional default columns for the _User collection (in addition to DefaultCols)
|
||||
_Session: {
|
||||
"restricted": {type:'Boolean'},
|
||||
"user": {type:'Pointer', className:'_User'},
|
||||
"user": {type:'Pointer', targetClass:'_User'},
|
||||
"installationId": {type:'String'},
|
||||
"sessionToken": {type:'String'},
|
||||
"expiresAt": {type:'Date'},
|
||||
@@ -73,7 +73,8 @@ var defaultColumns = {
|
||||
|
||||
|
||||
var requiredColumns = {
|
||||
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"]
|
||||
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
|
||||
_Role: ["name", "ACL"]
|
||||
}
|
||||
|
||||
// Valid classes must:
|
||||
@@ -307,8 +308,12 @@ function mongoFieldTypeToSchemaAPIType(type) {
|
||||
// is done in mongoSchemaFromFieldsAndClassName.
|
||||
function buildMergedSchemaObject(mongoObject, putRequest) {
|
||||
var newSchema = {};
|
||||
let sysSchemaField = Object.keys(defaultColumns).indexOf(mongoObject._id) === -1 ? [] : Object.keys(defaultColumns[mongoObject._id]);
|
||||
for (var oldField in mongoObject) {
|
||||
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
|
||||
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) {
|
||||
continue;
|
||||
}
|
||||
var fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'
|
||||
if (!fieldIsDeleted) {
|
||||
newSchema[oldField] = mongoFieldTypeToSchemaAPIType(mongoObject[oldField]);
|
||||
@@ -317,6 +322,9 @@ function buildMergedSchemaObject(mongoObject, putRequest) {
|
||||
}
|
||||
for (var newField in putRequest) {
|
||||
if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') {
|
||||
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) {
|
||||
continue;
|
||||
}
|
||||
newSchema[newField] = putRequest[newField];
|
||||
}
|
||||
}
|
||||
@@ -332,29 +340,22 @@ function buildMergedSchemaObject(mongoObject, putRequest) {
|
||||
// enabled) before calling this function.
|
||||
Schema.prototype.addClassIfNotExists = function(className, fields) {
|
||||
if (this.data[className]) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + className + ' already exists',
|
||||
});
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
||||
}
|
||||
|
||||
var mongoObject = mongoSchemaFromFieldsAndClassName(fields, className);
|
||||
|
||||
let mongoObject = mongoSchemaFromFieldsAndClassName(fields, className);
|
||||
if (!mongoObject.result) {
|
||||
return Promise.reject(mongoObject);
|
||||
}
|
||||
|
||||
return this.collection.insertOne(mongoObject.result)
|
||||
.then(result => result.ops[0])
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { //Mongo's duplicate key error
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + className + ' already exists',
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
.then(result => result.ops[0])
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { //Mongo's duplicate key error
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
// Returns a promise that resolves successfully to the new schema
|
||||
@@ -500,80 +501,47 @@ Schema.prototype.validateField = function(className, key, type, freeze) {
|
||||
|
||||
// Passing the database and prefix is necessary in order to drop relation collections
|
||||
// and remove fields from objects. Ideally the database would belong to
|
||||
// a database adapter and this fuction would close over it or access it via member.
|
||||
Schema.prototype.deleteField = function(fieldName, className, database, prefix) {
|
||||
// a database adapter and this function would close over it or access it via member.
|
||||
Schema.prototype.deleteField = function(fieldName, className, database) {
|
||||
if (!classNameIsValid(className)) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: invalidClassNameMessage(className),
|
||||
});
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
|
||||
}
|
||||
|
||||
if (!fieldNameIsValid(fieldName)) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'invalid field name: ' + fieldName,
|
||||
});
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`);
|
||||
}
|
||||
|
||||
//Don't allow deleting the default fields.
|
||||
if (!fieldNameIsValidForClass(fieldName, className)) {
|
||||
return Promise.reject({
|
||||
code: 136,
|
||||
error: 'field ' + fieldName + ' cannot be changed',
|
||||
});
|
||||
throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
|
||||
}
|
||||
|
||||
return this.reload()
|
||||
.then(schema => {
|
||||
return schema.hasClass(className)
|
||||
.then(hasClass => {
|
||||
if (!hasClass) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + className + ' does not exist',
|
||||
});
|
||||
}
|
||||
.then(schema => {
|
||||
return schema.hasClass(className)
|
||||
.then(hasClass => {
|
||||
if (!hasClass) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
}
|
||||
if (!schema.data[className][fieldName]) {
|
||||
throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
|
||||
}
|
||||
|
||||
if (!schema.data[className][fieldName]) {
|
||||
return Promise.reject({
|
||||
code: 255,
|
||||
error: 'field ' + fieldName + ' does not exist, cannot delete',
|
||||
});
|
||||
}
|
||||
if (schema.data[className][fieldName].startsWith('relation<')) {
|
||||
//For relations, drop the _Join table
|
||||
return database.dropCollection(`_Join:${fieldName}:${className}`);
|
||||
}
|
||||
|
||||
if (schema.data[className][fieldName].startsWith('relation<')) {
|
||||
//For relations, drop the _Join table
|
||||
return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className)
|
||||
//Save the _SCHEMA object
|
||||
// for non-relations, remove all the data.
|
||||
// This is necessary to ensure that the data is still gone if they add the same field.
|
||||
return database.collection(className)
|
||||
.then(collection => {
|
||||
var mongoFieldName = schema.data[className][fieldName].startsWith('*') ? '_p_' + fieldName : fieldName;
|
||||
return collection.update({}, { "$unset": { [mongoFieldName] : null } }, { multi: true });
|
||||
});
|
||||
})
|
||||
// Save the _SCHEMA object
|
||||
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}));
|
||||
} else {
|
||||
//for non-relations, remove all the data. This is necessary to ensure that the data is still gone
|
||||
//if they add the same field.
|
||||
return new Promise((resolve, reject) => {
|
||||
database.collection(prefix + className, (err, coll) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
var mongoFieldName = schema.data[className][fieldName].startsWith('*') ?
|
||||
'_p_' + fieldName :
|
||||
fieldName;
|
||||
return coll.update({}, {
|
||||
"$unset": { [mongoFieldName] : null },
|
||||
}, {
|
||||
multi: true,
|
||||
})
|
||||
//Save the _SCHEMA object
|
||||
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Given a schema promise, construct another schema promise that
|
||||
// validates this field once the schema loads.
|
||||
@@ -626,7 +594,7 @@ Schema.prototype.validateRequiredColumns = function(className, object, query) {
|
||||
if (!columns || columns.length == 0) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
|
||||
var missingColumns = columns.filter(function(column){
|
||||
if (query && query.objectId) {
|
||||
if (object[column] && typeof object[column] === "object") {
|
||||
@@ -636,15 +604,15 @@ Schema.prototype.validateRequiredColumns = function(className, object, query) {
|
||||
// Not trying to do anything there
|
||||
return false;
|
||||
}
|
||||
return !object[column]
|
||||
return !object[column]
|
||||
});
|
||||
|
||||
|
||||
if (missingColumns.length > 0) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INCORRECT_TYPE,
|
||||
missingColumns[0]+' is required.');
|
||||
}
|
||||
|
||||
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
@@ -731,19 +699,31 @@ function getObjectType(obj) {
|
||||
if (obj instanceof Array) {
|
||||
return 'array';
|
||||
}
|
||||
if (obj.__type === 'Pointer' && obj.className) {
|
||||
return '*' + obj.className;
|
||||
}
|
||||
if (obj.__type === 'File' && obj.name) {
|
||||
return 'file';
|
||||
}
|
||||
if (obj.__type === 'Date' && obj.iso) {
|
||||
return 'date';
|
||||
}
|
||||
if (obj.__type == 'GeoPoint' &&
|
||||
obj.latitude != null &&
|
||||
obj.longitude != null) {
|
||||
return 'geopoint';
|
||||
if (obj.__type){
|
||||
switch(obj.__type) {
|
||||
case 'Pointer' :
|
||||
if(obj.className) {
|
||||
return '*' + obj.className;
|
||||
}
|
||||
case 'File' :
|
||||
if(obj.name) {
|
||||
return 'file';
|
||||
}
|
||||
case 'Date' :
|
||||
if(obj.iso) {
|
||||
return 'date';
|
||||
}
|
||||
case 'GeoPoint' :
|
||||
if(obj.latitude != null && obj.longitude != null) {
|
||||
return 'geopoint';
|
||||
}
|
||||
case 'Bytes' :
|
||||
if(obj.base64) {
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid "+obj.__type);
|
||||
}
|
||||
}
|
||||
if (obj['$ne']) {
|
||||
return getObjectType(obj['$ne']);
|
||||
|
||||
58
src/cache.js
58
src/cache.js
@@ -1,45 +1,35 @@
|
||||
export var apps = {};
|
||||
export var stats = {};
|
||||
export var isLoaded = false;
|
||||
export var users = {};
|
||||
/** @flow weak */
|
||||
|
||||
export function getApp(app, callback) {
|
||||
if (apps[app]) return callback(true, apps[app]);
|
||||
return callback(false);
|
||||
export function CacheStore<KeyType, ValueType>() {
|
||||
let dataStore: {[id:KeyType]:ValueType} = {};
|
||||
return {
|
||||
get: (key: KeyType): ValueType => {
|
||||
return dataStore[key];
|
||||
},
|
||||
set(key: KeyType, value: ValueType): void {
|
||||
dataStore[key] = value;
|
||||
},
|
||||
remove(key: KeyType): void {
|
||||
delete dataStore[key];
|
||||
},
|
||||
clear(): void {
|
||||
dataStore = {};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateStat(key, value) {
|
||||
stats[key] = value;
|
||||
}
|
||||
|
||||
export function getUser(sessionToken) {
|
||||
if (users[sessionToken]) return users[sessionToken];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function setUser(sessionToken, userObject) {
|
||||
users[sessionToken] = userObject;
|
||||
}
|
||||
|
||||
export function clearUser(sessionToken) {
|
||||
delete users[sessionToken];
|
||||
}
|
||||
const apps = CacheStore();
|
||||
const users = CacheStore();
|
||||
|
||||
//So far used only in tests
|
||||
export function clearCache() {
|
||||
apps = {};
|
||||
stats = {};
|
||||
users = {};
|
||||
export function clearCache(): void {
|
||||
apps.clear();
|
||||
users.clear();
|
||||
}
|
||||
|
||||
export default {
|
||||
apps,
|
||||
stats,
|
||||
isLoaded,
|
||||
getApp,
|
||||
updateStat,
|
||||
clearUser,
|
||||
getUser,
|
||||
setUser,
|
||||
users,
|
||||
clearCache,
|
||||
CacheStore
|
||||
};
|
||||
|
||||
@@ -42,14 +42,6 @@ if (program.args.length > 0 ) {
|
||||
console.log(`Configuation loaded from ${jsonPath}`)
|
||||
}
|
||||
|
||||
if (!program.appId || !program.masterKey || !program.serverURL) {
|
||||
program.outputHelp();
|
||||
console.error("");
|
||||
console.error(colors.red("ERROR: appId, masterKey and serverURL are required"));
|
||||
console.error("");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
options = Object.keys(definitions).reduce(function (options, key) {
|
||||
if (program[key]) {
|
||||
options[key] = program[key];
|
||||
@@ -61,6 +53,14 @@ if (!options.serverURL) {
|
||||
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
|
||||
}
|
||||
|
||||
if (!options.appId || !options.masterKey || !options.serverURL) {
|
||||
program.outputHelp();
|
||||
console.error("");
|
||||
console.error(colors.red("ERROR: appId, masterKey and serverURL are required"));
|
||||
console.error("");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const api = new ParseServer(options);
|
||||
app.use(options.mountPath, api);
|
||||
|
||||
@@ -36,6 +36,12 @@ module.exports = function(options) {
|
||||
options.followRedirect = options.followRedirects == true;
|
||||
|
||||
request(options, (error, response, body) => {
|
||||
if (error) {
|
||||
if (callbacks.error) {
|
||||
callbacks.error(error);
|
||||
}
|
||||
return promise.reject(error);
|
||||
}
|
||||
var httpResponse = {};
|
||||
httpResponse.status = response.statusCode;
|
||||
httpResponse.headers = response.headers;
|
||||
@@ -46,7 +52,7 @@ module.exports = function(options) {
|
||||
httpResponse.data = JSON.parse(response.body);
|
||||
} catch (e) {}
|
||||
// Consider <200 && >= 400 as errors
|
||||
if (error || httpResponse.status <200 || httpResponse.status >=400) {
|
||||
if (httpResponse.status < 200 || httpResponse.status >= 400) {
|
||||
if (callbacks.error) {
|
||||
callbacks.error(httpResponse);
|
||||
}
|
||||
|
||||
91
src/features.js
Normal file
91
src/features.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* features.js
|
||||
* Feature config file that holds information on the features that are currently
|
||||
* available on Parse Server. This is primarily created to work with an UI interface
|
||||
* like the web dashboard. The list of features will change depending on the your
|
||||
* app, choice of adapter as well as Parse Server version. This approach will enable
|
||||
* the dashboard to be built independently and still support these use cases.
|
||||
*
|
||||
*
|
||||
* Default features and feature options are listed in the features object.
|
||||
*
|
||||
* featureSwitch is a convenient way to turn on/off features without changing the config
|
||||
*
|
||||
* Features that use Adapters should specify the feature options through
|
||||
* the setFeature method in your controller and feature
|
||||
* Reference PushController and ParsePushAdapter as an example.
|
||||
*
|
||||
* NOTE: When adding new endpoints be sure to update this list both (features, featureSwitch)
|
||||
* if you are planning to have a UI consume it.
|
||||
*/
|
||||
|
||||
// default features
|
||||
let features = {
|
||||
globalConfig: {
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
},
|
||||
hooks: {
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
},
|
||||
logs: {
|
||||
level: false,
|
||||
size: false,
|
||||
order: false,
|
||||
until: false,
|
||||
from: false,
|
||||
},
|
||||
push: {
|
||||
immediatePush: false,
|
||||
scheduledPush: false,
|
||||
storedPushData: false,
|
||||
pushAudiences: false,
|
||||
},
|
||||
schemas: {
|
||||
addField: true,
|
||||
removeField: true,
|
||||
addClass: true,
|
||||
removeClass: true,
|
||||
clearAllDataFromClass: false,
|
||||
exportClass: false,
|
||||
},
|
||||
};
|
||||
|
||||
// master switch for features
|
||||
let featuresSwitch = {
|
||||
globalConfig: true,
|
||||
hooks: true,
|
||||
logs: true,
|
||||
push: true,
|
||||
schemas: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* set feature config options
|
||||
*/
|
||||
function setFeature(key, value) {
|
||||
features[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* get feature config options
|
||||
*/
|
||||
function getFeatures() {
|
||||
let result = {};
|
||||
Object.keys(features).forEach((key) => {
|
||||
if (featuresSwitch[key] && features[key]) {
|
||||
result[key] = features[key];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getFeatures,
|
||||
setFeature,
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
// global_config.js
|
||||
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
var router = new PromiseRouter();
|
||||
|
||||
function getGlobalConfig(req) {
|
||||
return req.config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.findOne({'_id': 1}))
|
||||
.then(globalConfig => ({response: { params: globalConfig.params }}))
|
||||
.catch(() => ({
|
||||
status: 404,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'config does not exist',
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function updateGlobalConfig(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return Promise.resolve({
|
||||
status: 401,
|
||||
response: {error: 'unauthorized'},
|
||||
});
|
||||
}
|
||||
|
||||
return req.config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body }))
|
||||
.then(response => {
|
||||
return { response: { result: true } }
|
||||
})
|
||||
.catch(() => ({
|
||||
status: 404,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'config cannot be updated',
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
router.route('GET', '/config', getGlobalConfig);
|
||||
router.route('PUT', '/config', updateGlobalConfig);
|
||||
|
||||
module.exports = router;
|
||||
137
src/index.js
137
src/index.js
@@ -10,43 +10,48 @@ var batch = require('./batch'),
|
||||
multer = require('multer'),
|
||||
Parse = require('parse/node').Parse;
|
||||
|
||||
//import passwordReset from './passwordReset';
|
||||
import cache from './cache';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
||||
import { S3Adapter } from './Adapters/Files/S3Adapter';
|
||||
import { GCSAdapter } from './Adapters/Files/GCSAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
|
||||
import Config from './Config';
|
||||
import parseServerPackage from '../package.json';
|
||||
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
|
||||
import { PushController } from './Controllers/PushController';
|
||||
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { LogsRouter } from './Routers/LogsRouter';
|
||||
import { HooksRouter } from './Routers/HooksRouter';
|
||||
|
||||
import { loadAdapter } from './Adapters/AdapterLoader';
|
||||
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
|
||||
import { LoggerController } from './Controllers/LoggerController';
|
||||
import { HooksController } from './Controllers/HooksController';
|
||||
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { FeaturesRouter } from './Routers/FeaturesRouter';
|
||||
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
import { GCSAdapter } from './Adapters/Files/GCSAdapter';
|
||||
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
|
||||
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
||||
import { HooksController } from './Controllers/HooksController';
|
||||
import { HooksRouter } from './Routers/HooksRouter';
|
||||
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
|
||||
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
||||
import { loadAdapter } from './Adapters/AdapterLoader';
|
||||
import { LoggerController } from './Controllers/LoggerController';
|
||||
import { LogsRouter } from './Routers/LogsRouter';
|
||||
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
|
||||
import { PushController } from './Controllers/PushController';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { randomString } from './cryptoUtils';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
import { S3Adapter } from './Adapters/Files/S3Adapter';
|
||||
import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { setFeature } from './features';
|
||||
import { UserController } from './Controllers/UserController';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
addParseCloud();
|
||||
|
||||
// ParseServer works like a constructor of an express app.
|
||||
// The args that we understand are:
|
||||
// "databaseAdapter": a class like ExportAdapter providing create, find,
|
||||
// "databaseAdapter": a class like DatabaseController providing create, find,
|
||||
// update, and delete
|
||||
// "filesAdapter": a class like GridStoreAdapter providing create, get,
|
||||
// and delete
|
||||
@@ -73,11 +78,12 @@ addParseCloud();
|
||||
function ParseServer({
|
||||
appId = requiredParameter('You must provide an appId!'),
|
||||
masterKey = requiredParameter('You must provide a masterKey!'),
|
||||
appName,
|
||||
databaseAdapter,
|
||||
filesAdapter,
|
||||
push,
|
||||
loggerAdapter,
|
||||
databaseURI,
|
||||
databaseURI = DatabaseAdapter.defaultDatabaseURI,
|
||||
cloud,
|
||||
collectionPrefix = '',
|
||||
clientKey,
|
||||
@@ -90,9 +96,18 @@ function ParseServer({
|
||||
allowClientClassCreation = true,
|
||||
oauth = {},
|
||||
serverURL = requiredParameter('You must provide a serverURL!'),
|
||||
maxUploadSize = '20mb'
|
||||
maxUploadSize = '20mb',
|
||||
verifyUserEmails = false,
|
||||
emailAdapter,
|
||||
publicServerURL,
|
||||
customPages = {
|
||||
invalidLink: undefined,
|
||||
verifyEmailSuccess: undefined,
|
||||
choosePassword: undefined,
|
||||
passwordResetSuccess: undefined
|
||||
},
|
||||
}) {
|
||||
|
||||
setFeature('serverVersion', parseServerPackage.version);
|
||||
// Initialize the node client SDK automatically
|
||||
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
|
||||
Parse.serverURL = serverURL;
|
||||
@@ -116,19 +131,24 @@ function ParseServer({
|
||||
}
|
||||
}
|
||||
|
||||
const filesControllerAdapter = loadAdapter(filesAdapter, GridStoreAdapter);
|
||||
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
|
||||
return new GridStoreAdapter(databaseURI);
|
||||
});
|
||||
const pushControllerAdapter = loadAdapter(push, ParsePushAdapter);
|
||||
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
|
||||
|
||||
const emailControllerAdapter = loadAdapter(emailAdapter);
|
||||
// We pass the options and the base class for the adatper,
|
||||
// Note that passing an instance would work too
|
||||
const filesController = new FilesController(filesControllerAdapter);
|
||||
const pushController = new PushController(pushControllerAdapter);
|
||||
const loggerController = new LoggerController(loggerControllerAdapter);
|
||||
const filesController = new FilesController(filesControllerAdapter, appId);
|
||||
const pushController = new PushController(pushControllerAdapter, appId);
|
||||
const loggerController = new LoggerController(loggerControllerAdapter, appId);
|
||||
const hooksController = new HooksController(appId, collectionPrefix);
|
||||
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
|
||||
|
||||
cache.apps[appId] = {
|
||||
|
||||
cache.apps.set(appId, {
|
||||
masterKey: masterKey,
|
||||
serverURL: serverURL,
|
||||
collectionPrefix: collectionPrefix,
|
||||
clientKey: clientKey,
|
||||
javascriptKey: javascriptKey,
|
||||
@@ -140,25 +160,34 @@ function ParseServer({
|
||||
pushController: pushController,
|
||||
loggerController: loggerController,
|
||||
hooksController: hooksController,
|
||||
userController: userController,
|
||||
verifyUserEmails: verifyUserEmails,
|
||||
enableAnonymousUsers: enableAnonymousUsers,
|
||||
allowClientClassCreation: allowClientClassCreation,
|
||||
oauth: oauth
|
||||
};
|
||||
oauth: oauth,
|
||||
appName: appName,
|
||||
publicServerURL: publicServerURL,
|
||||
customPages: customPages,
|
||||
});
|
||||
|
||||
// To maintain compatibility. TODO: Remove in v2.1
|
||||
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
|
||||
if (process.env.FACEBOOK_APP_ID) {
|
||||
cache.apps[appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
|
||||
cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
|
||||
}
|
||||
|
||||
Config.validate(cache.apps.get(appId));
|
||||
|
||||
// This app serves the Parse API directly.
|
||||
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
|
||||
var api = express();
|
||||
|
||||
//api.use("/apps", express.static(__dirname + "/public"));
|
||||
// File handling needs to be before default middlewares are applied
|
||||
api.use('/', new FilesRouter().getExpressRouter({
|
||||
maxUploadSize: maxUploadSize
|
||||
}));
|
||||
|
||||
api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressApp());
|
||||
|
||||
// TODO: separate this from the regular ParseServer object
|
||||
if (process.env.TESTING == 1) {
|
||||
api.use('/', require('./testing-routes').router);
|
||||
@@ -180,24 +209,27 @@ function ParseServer({
|
||||
new SchemasRouter(),
|
||||
new PushRouter(),
|
||||
new LogsRouter(),
|
||||
new IAPValidationRouter()
|
||||
new IAPValidationRouter(),
|
||||
new FeaturesRouter(),
|
||||
];
|
||||
|
||||
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
|
||||
routers.push(require('./global_config'));
|
||||
routers.push(new GlobalConfigRouter());
|
||||
}
|
||||
|
||||
if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) {
|
||||
routers.push(new HooksRouter());
|
||||
}
|
||||
|
||||
let appRouter = new PromiseRouter();
|
||||
routers.forEach((router) => {
|
||||
appRouter.merge(router);
|
||||
});
|
||||
let routes = routers.reduce((memo, router) => {
|
||||
return memo.concat(router.routes);
|
||||
}, []);
|
||||
|
||||
let appRouter = new PromiseRouter(routes);
|
||||
|
||||
batch.mountOnto(appRouter);
|
||||
|
||||
appRouter.mountOnto(api);
|
||||
api.use(appRouter.expressApp());
|
||||
|
||||
api.use(middlewares.handleParseErrors);
|
||||
|
||||
@@ -221,13 +253,6 @@ function addParseCloud() {
|
||||
global.Parse = Parse;
|
||||
}
|
||||
|
||||
function getClassName(parseClass) {
|
||||
if (parseClass && parseClass.className) {
|
||||
return parseClass.className;
|
||||
}
|
||||
return parseClass;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ParseServer: ParseServer,
|
||||
S3Adapter: S3Adapter,
|
||||
|
||||
@@ -35,7 +35,7 @@ function handleParseHeaders(req, res, next) {
|
||||
|
||||
var fileViaJSON = false;
|
||||
|
||||
if (!info.appId || !cache.apps[info.appId]) {
|
||||
if (!info.appId || !cache.apps.get(info.appId)) {
|
||||
// See if we can find the app id on the body.
|
||||
if (req.body instanceof Buffer) {
|
||||
// The only chance to find the app id is if this is a file
|
||||
@@ -44,12 +44,10 @@ function handleParseHeaders(req, res, next) {
|
||||
fileViaJSON = true;
|
||||
}
|
||||
|
||||
if (req.body && req.body._ApplicationId
|
||||
&& cache.apps[req.body._ApplicationId]
|
||||
&& (
|
||||
!info.masterKey
|
||||
||
|
||||
cache.apps[req.body._ApplicationId]['masterKey'] === info.masterKey)
|
||||
if (req.body &&
|
||||
req.body._ApplicationId &&
|
||||
cache.apps.get(req.body._ApplicationId) &&
|
||||
(!info.masterKey || cache.apps.get(req.body._ApplicationId).masterKey === info.masterKey)
|
||||
) {
|
||||
info.appId = req.body._ApplicationId;
|
||||
info.javascriptKey = req.body._JavaScriptKey || '';
|
||||
@@ -84,15 +82,14 @@ function handleParseHeaders(req, res, next) {
|
||||
req.body = new Buffer(base64, 'base64');
|
||||
}
|
||||
|
||||
info.app = cache.apps[info.appId];
|
||||
info.app = cache.apps.get(info.appId);
|
||||
req.config = new Config(info.appId, mount);
|
||||
req.database = req.config.database;
|
||||
req.info = info;
|
||||
|
||||
var isMaster = (info.masterKey === req.config.masterKey);
|
||||
|
||||
if (isMaster) {
|
||||
req.auth = new auth.Auth(req.config, true);
|
||||
req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: true });
|
||||
next();
|
||||
return;
|
||||
}
|
||||
@@ -117,23 +114,23 @@ function handleParseHeaders(req, res, next) {
|
||||
}
|
||||
|
||||
if (!info.sessionToken) {
|
||||
req.auth = new auth.Auth(req.config, false);
|
||||
req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: false });
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
return auth.getAuthForSessionToken(
|
||||
req.config, info.sessionToken).then((auth) => {
|
||||
return auth.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken })
|
||||
.then((auth) => {
|
||||
if (auth) {
|
||||
req.auth = auth;
|
||||
next();
|
||||
}
|
||||
}).catch((error) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
// TODO: Determine the correct error scenario.
|
||||
console.log(error);
|
||||
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
var allowCrossDomain = function(req, res, next) {
|
||||
@@ -177,6 +174,9 @@ var handleParseErrors = function(err, req, res, next) {
|
||||
|
||||
res.status(httpStatus);
|
||||
res.json({code: err.code, error: err.message});
|
||||
} else if (err.status && err.message) {
|
||||
res.status(err.status);
|
||||
res.json({error: err.message});
|
||||
} else {
|
||||
console.log('Uncaught internal server error.', err, err.stack);
|
||||
res.status(500);
|
||||
@@ -194,6 +194,16 @@ function enforceMasterKeyAccess(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
function promiseEnforceMasterKeyAccess(request) {
|
||||
if (!request.auth.isMaster) {
|
||||
let error = new Error();
|
||||
error.status = 403;
|
||||
error.message = "unauthorized: master key is required";
|
||||
throw error;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function invalidRequest(req, res) {
|
||||
res.status(403);
|
||||
res.end('{"error":"unauthorized"}');
|
||||
@@ -204,5 +214,6 @@ module.exports = {
|
||||
allowMethodOverride: allowMethodOverride,
|
||||
handleParseErrors: handleParseErrors,
|
||||
handleParseHeaders: handleParseHeaders,
|
||||
enforceMasterKeyAccess: enforceMasterKeyAccess
|
||||
enforceMasterKeyAccess: enforceMasterKeyAccess,
|
||||
promiseEnforceMasterKeyAccess
|
||||
};
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/* @flow */
|
||||
export default (errorMessage: string) => {throw errorMessage}
|
||||
/** @flow */
|
||||
export default (errorMessage: string): any => { throw errorMessage }
|
||||
|
||||
@@ -46,7 +46,7 @@ function del(config, auth, className, objectId) {
|
||||
.then((response) => {
|
||||
if (response && response.results && response.results.length) {
|
||||
response.results[0].className = className;
|
||||
cache.clearUser(response.results[0].sessionToken);
|
||||
cache.users.remove(response.results[0].sessionToken);
|
||||
inflatedObject = Parse.Object.fromJSON(response.results[0]);
|
||||
return triggers.maybeRunTrigger(triggers.Types.beforeDelete, auth, inflatedObject, null, config.applicationId);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ var router = express.Router();
|
||||
// creates a unique app in the cache, with a collection prefix
|
||||
function createApp(req, res) {
|
||||
var appId = cryptoUtils.randomHexString(32);
|
||||
cache.apps[appId] = {
|
||||
// TODO: (nlutsenko) This doesn't work and should die, since there are no controllers on this configuration.
|
||||
cache.apps.set(appId, {
|
||||
'collectionPrefix': appId + '_',
|
||||
'masterKey': 'master'
|
||||
};
|
||||
});
|
||||
var keys = {
|
||||
'application_id': appId,
|
||||
'client_key': 'unused',
|
||||
@@ -31,7 +32,7 @@ function clearApp(req, res) {
|
||||
if (!req.auth.isMaster) {
|
||||
return res.status(401).send({"error": "unauthorized"});
|
||||
}
|
||||
req.database.deleteEverything().then(() => {
|
||||
return req.config.database.deleteEverything().then(() => {
|
||||
res.status(200).send({});
|
||||
});
|
||||
}
|
||||
@@ -41,8 +42,8 @@ function dropApp(req, res) {
|
||||
if (!req.auth.isMaster) {
|
||||
return res.status(401).send({"error": "unauthorized"});
|
||||
}
|
||||
req.database.deleteEverything().then(() => {
|
||||
delete cache.apps[req.config.applicationId];
|
||||
return req.config.database.deleteEverything().then(() => {
|
||||
cache.apps.remove(req.config.applicationId);
|
||||
res.status(200).send({});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,6 +42,12 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
||||
key = '_updated_at';
|
||||
timeField = true;
|
||||
break;
|
||||
case '_email_verify_token':
|
||||
key = "_email_verify_token";
|
||||
break;
|
||||
case '_perishable_token':
|
||||
key = "_perishable_token";
|
||||
break;
|
||||
case 'sessionToken':
|
||||
case '_session_token':
|
||||
key = '_session_token';
|
||||
@@ -638,7 +644,7 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
|
||||
break;
|
||||
case 'expiresAt':
|
||||
case '_expiresAt':
|
||||
restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key])).iso;
|
||||
restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key]));
|
||||
break;
|
||||
default:
|
||||
// Check other auth data keys
|
||||
@@ -649,7 +655,7 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
|
||||
restObject['authData'][provider] = mongoObject[key];
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (key.indexOf('_p_') == 0) {
|
||||
var newKey = key.substring(3);
|
||||
var expected;
|
||||
|
||||
@@ -110,12 +110,11 @@ export function getRequestObject(triggerType, auth, parseObject, originalParseOb
|
||||
if (auth.user) {
|
||||
request['user'] = auth.user;
|
||||
}
|
||||
// TODO: Add installation to Auth?
|
||||
if (auth.installationId) {
|
||||
request['installationId'] = auth.installationId;
|
||||
}
|
||||
return request;
|
||||
};
|
||||
}
|
||||
|
||||
// Creates the response object, and uses the request object to pass data
|
||||
// The API will call this with REST API formatted objects, this will
|
||||
@@ -157,8 +156,8 @@ export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObj
|
||||
var response = getResponseObject(request, resolve, reject);
|
||||
// Force the current Parse app before the trigger
|
||||
Parse.applicationId = applicationId;
|
||||
Parse.javascriptKey = cache.apps[applicationId].javascriptKey || '';
|
||||
Parse.masterKey = cache.apps[applicationId].masterKey;
|
||||
Parse.javascriptKey = cache.apps.get(applicationId).javascriptKey || '';
|
||||
Parse.masterKey = cache.apps.get(applicationId).masterKey;
|
||||
trigger(request, response);
|
||||
});
|
||||
};
|
||||
|
||||
176
views/choose_password
Normal file
176
views/choose_password
Normal file
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!-- This page is displayed when someone clicks a valid 'reset password' link.
|
||||
Users should feel free to add to this page (i.e. branding or security widgets)
|
||||
but should be sure not to delete any of the form inputs or the javascript from the
|
||||
template file. This javascript is what adds the necessary values to authenticate
|
||||
this session with Parse.
|
||||
The query params 'username' and 'app' hold the friendly names for your current user and
|
||||
your app. You should feel free to incorporate their values to make the page more personal.
|
||||
If you are missing form parameters in your POST, Parse will navigate back to this page and
|
||||
add an 'error' query parameter.
|
||||
-->
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
<style type='text/css'>
|
||||
h1 {
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 45px 0px 45px 0px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
padding: 0px 8px 0px 8px;
|
||||
margin: -25px 0px -20px 0px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
color: #0067AB;
|
||||
margin: 15px 99px 0px 98px;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #666666;
|
||||
}
|
||||
form {
|
||||
margin: 0px 0px 45px 0px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
}
|
||||
form > * {
|
||||
display: block;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 22px;
|
||||
color: white;
|
||||
background: #0067AB;
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
-o-border-radius: 5px;
|
||||
-ms-border-radius: 5px;
|
||||
-khtml-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
background-image: -webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#0070BA),color-stop(100%,#00558C));
|
||||
background-image: -webkit-linear-gradient(#0070BA,#00558C);
|
||||
background-image: -moz-linear-gradient(#0070BA,#00558C);
|
||||
background-image: -o-linear-gradient(#0070BA,#00558C);
|
||||
background-image: -ms-linear-gradient(#0070BA,#00558C);
|
||||
background-image: linear-gradient(#0070BA,#00558C);
|
||||
-moz-box-shadow: inset 0 1px 0 0 #0076c4;
|
||||
-webkit-box-shadow: inset 0 1px 0 0 #0076c4;
|
||||
-o-box-shadow: inset 0 1px 0 0 #0076c4;
|
||||
box-shadow: inset 0 1px 0 0 #0076c4;
|
||||
border: 1px solid #005E9C;
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
display: block;
|
||||
font-family: "Helvetica Neue",Helvetica;
|
||||
|
||||
-webkit-box-align: center;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
line-height: normal;
|
||||
text-transform: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-image: -webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#0079CA),color-stop(100%,#005E9C));
|
||||
background-image: -webkit-linear-gradient(#0079CA,#005E9C);
|
||||
background-image: -moz-linear-gradient(#0079CA,#005E9C);
|
||||
background-image: -o-linear-gradient(#0079CA,#005E9C);
|
||||
background-image: -ms-linear-gradient(#0079CA,#005E9C);
|
||||
background-image: linear-gradient(#0079CA,#005E9C);
|
||||
-moz-box-shadow: inset 0 0 0 0 #0076c4;
|
||||
-webkit-box-shadow: inset 0 0 0 0 #0076c4;
|
||||
-o-box-shadow: inset 0 0 0 0 #0076c4;
|
||||
box-shadow: inset 0 0 0 0 #0076c4;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-image: -webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#00395E),color-stop(100%,#005891));
|
||||
background-image: -webkit-linear-gradient(#00395E,#005891);
|
||||
background-image: -moz-linear-gradient(#00395E,#005891);
|
||||
background-image: -o-linear-gradient(#00395E,#005891);
|
||||
background-image: -ms-linear-gradient(#00395E,#005891);
|
||||
background-image: linear-gradient(#00395E,#005891);
|
||||
}
|
||||
|
||||
input {
|
||||
color: black;
|
||||
cursor: auto;
|
||||
display: inline-block;
|
||||
font-family: 'Helvetica Neue', Helvetica;
|
||||
font-size: 25px;
|
||||
height: 30px;
|
||||
letter-spacing: normal;
|
||||
line-height: normal;
|
||||
margin: 2px 0px 2px 0px;
|
||||
padding: 5px;
|
||||
text-transform: none;
|
||||
vertical-align: baseline;
|
||||
width: 500px;
|
||||
word-spacing: 0px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Reset Your Password<span id='app'></span></h1>
|
||||
<noscript>We apologize, but resetting your password requires javascript</noscript>
|
||||
<div class='error' id='error'></div>
|
||||
<form id='form' action='#' method='POST'>
|
||||
<label>New Password for <span id='username_label'></span></label>
|
||||
<input name="new_password" type="password" />
|
||||
<input name='utf-8' type='hidden' value='✓' />
|
||||
<input name="username" id="username" type="hidden" />
|
||||
<input name="token" id="token" type="hidden" />
|
||||
<button>Change Password</button>
|
||||
</form>
|
||||
|
||||
<script language='javascript' type='text/javascript'>
|
||||
<!--
|
||||
window.onload = function() {
|
||||
var urlParams = {};
|
||||
(function () {
|
||||
var pair, // Really a match. Index 0 is the full match; 1 & 2 are the key & val.
|
||||
tokenize = /([^&=]+)=?([^&]*)/g,
|
||||
// decodeURIComponents escapes everything but will leave +s that should be ' '
|
||||
re_space = function (s) { return decodeURIComponent(s.replace(/\+/g, " ")); },
|
||||
// Substring to cut off the leading '?'
|
||||
querystring = window.location.search.substring(1);
|
||||
|
||||
while (pair = tokenize.exec(querystring))
|
||||
urlParams[re_space(pair[1])] = re_space(pair[2]);
|
||||
})();
|
||||
|
||||
var id = urlParams['id'];
|
||||
var base = PARSE_SERVER_URL;
|
||||
document.getElementById('form').setAttribute('action', base + '/apps/' + id + '/request_password_reset');
|
||||
document.getElementById('username').value = urlParams['username'];
|
||||
document.getElementById('username_label').appendChild(document.createTextNode(urlParams['username']));
|
||||
|
||||
document.getElementById('token').value = urlParams['token'];
|
||||
if (urlParams['error']) {
|
||||
document.getElementById('error').appendChild(document.createTextNode(urlParams['error']));
|
||||
}
|
||||
if (urlParams['app']) {
|
||||
document.getElementById('app').appendChild(document.createTextNode(' for ' + urlParams['app']));
|
||||
}
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
</body>
|
||||
Reference in New Issue
Block a user