├── spec
├── MockAdapter.js
├── support
│ └── jasmine.json
├── ExportAdapter.spec.js
├── myoauth.js
├── FilesController.spec.js
├── index.spec.js
├── LogsRouter.spec.js
├── FileLoggerAdapter.spec.js
├── ParseRole.spec.js
├── AdapterLoader.spec.js
├── cryptoUtils.spec.js
├── ParseGlobalConfig.spec.js
├── LoggerController.spec.js
├── cloud
│ └── main.js
├── AdaptableController.spec.js
├── PushRouter.spec.js
├── PushController.spec.js
├── ParsePushAdapter.spec.js
├── RestQuery.spec.js
├── GCM.spec.js
├── HTTPRequest.spec.js
├── PurchaseValidation.spec.js
├── RestCreate.spec.js
└── OneSignalPushAdapter.spec.js
├── src
├── requiredParameter.js
├── Adapters
│ ├── Logger
│ │ ├── LoggerAdapter.js
│ │ └── FileLoggerAdapter.js
│ ├── Push
│ │ ├── PushAdapter.js
│ │ ├── PushAdapterUtils.js
│ │ ├── ParsePushAdapter.js
│ │ └── OneSignalPushAdapter.js
│ ├── Files
│ │ ├── FilesAdapter.js
│ │ ├── GridStoreAdapter.js
│ │ └── S3Adapter.js
│ └── AdapterLoader.js
├── oauth
│ ├── index.js
│ ├── twitter.js
│ ├── google.js
│ ├── instagram.js
│ ├── meetup.js
│ ├── github.js
│ ├── linkedin.js
│ └── facebook.js
├── Routers
│ ├── AnalyticsRouter.js
│ ├── RolesRouter.js
│ ├── FunctionsRouter.js
│ ├── LogsRouter.js
│ ├── InstallationsRouter.js
│ ├── SessionsRouter.js
│ ├── PushRouter.js
│ ├── FilesRouter.js
│ ├── IAPValidationRouter.js
│ ├── ClassesRouter.js
│ └── UsersRouter.js
├── cache.js
├── password.js
├── global_config.js
├── Config.js
├── cryptoUtils.js
├── Controllers
│ ├── AdaptableController.js
│ ├── FilesController.js
│ ├── LoggerController.js
│ └── PushController.js
├── DatabaseAdapter.js
├── httpRequest.js
├── testing-routes.js
├── batch.js
├── triggers.js
├── GCM.js
├── rest.js
├── PromiseRouter.js
├── Auth.js
└── middlewares.js
├── .github
└── parse-server-logo.png
├── .flowconfig
├── jsconfig.json
├── .babelrc
├── .travis.yml
├── .npmignore
├── .gitignore
├── CONTRIBUTING.md
├── bin
├── dev
└── parse-server
├── LICENSE
├── package.json
├── PATENTS
├── CHANGELOG.md
└── README.md
/spec/MockAdapter.js:
--------------------------------------------------------------------------------
1 | module.exports = function(options) {
2 | this.options = options;
3 | }
4 |
--------------------------------------------------------------------------------
/src/requiredParameter.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | export default (errorMessage: string) => {throw errorMessage}
3 |
--------------------------------------------------------------------------------
/.github/parse-server-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Todo/parse-server/master/.github/parse-server-logo.png
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | .*/node_modules/
3 | .*/lib/
4 |
5 | [include]
6 |
7 | [libs]
8 |
9 | [options]
10 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "transform-flow-strip-types"
4 | ],
5 | "presets": [
6 | "es2015",
7 | "stage-0"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/spec/support/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "spec",
3 | "spec_files": [
4 | "*spec.js"
5 | ],
6 | "helpers": [
7 | "../node_modules/babel-core/register.js",
8 | "helper.js"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | branches:
2 | only:
3 | - master
4 | language: node_js
5 | node_js:
6 | - "4.3"
7 | env:
8 | - MONGODB_VERSION=2.6.11
9 | - MONGODB_VERSION=3.0.8
10 | cache:
11 | directories:
12 | - $HOME/.mongodb/versions/downloads
13 | after_success: ./node_modules/.bin/codecov
14 |
--------------------------------------------------------------------------------
/spec/ExportAdapter.spec.js:
--------------------------------------------------------------------------------
1 | var ExportAdapter = require('../src/ExportAdapter');
2 |
3 | describe('ExportAdapter', () => {
4 | it('can be constructed', (done) => {
5 | var database = new ExportAdapter('mongodb://localhost:27017/test',
6 | {
7 | collectionPrefix: 'test_'
8 | });
9 | database.connect().then(done, (error) => {
10 | console.log('error', error.stack);
11 | fail();
12 | });
13 | });
14 |
15 | });
16 |
--------------------------------------------------------------------------------
/src/Adapters/Logger/LoggerAdapter.js:
--------------------------------------------------------------------------------
1 | // Logger Adapter
2 | //
3 | // Allows you to change the logger mechanism
4 | //
5 | // Adapter classes must implement the following functions:
6 | // * info(obj1 [, obj2, .., objN])
7 | // * error(obj1 [, obj2, .., objN])
8 | // * query(options, callback)
9 | // Default is FileLoggerAdapter.js
10 |
11 | export class LoggerAdapter {
12 | info() {}
13 | error() {}
14 | query(options, callback) {}
15 | }
16 |
17 | export default LoggerAdapter;
18 |
--------------------------------------------------------------------------------
/src/oauth/index.js:
--------------------------------------------------------------------------------
1 | var facebook = require('./facebook');
2 | var instagram = require("./instagram");
3 | var linkedin = require("./linkedin");
4 | var meetup = require("./meetup");
5 | var google = require("./google");
6 | var github = require("./github");
7 | var twitter = require("./twitter");
8 |
9 | module.exports = {
10 | facebook: facebook,
11 | github: github,
12 | google: google,
13 | instagram: instagram,
14 | linkedin: linkedin,
15 | meetup: meetup,
16 | twitter: twitter
17 | }
--------------------------------------------------------------------------------
/spec/myoauth.js:
--------------------------------------------------------------------------------
1 | // Custom oauth provider by module
2 |
3 | // Returns a promise that fulfills iff this user id is valid.
4 | function validateAuthData(authData) {
5 | if (authData.id == "12345" && authData.access_token == "12345") {
6 | return Promise.resolve();
7 | }
8 | return Promise.reject();
9 | }
10 | function validateAppId() {
11 | return Promise.resolve();
12 | }
13 |
14 | module.exports = {
15 | validateAppId: validateAppId,
16 | validateAuthData: validateAuthData
17 | };
18 |
--------------------------------------------------------------------------------
/src/Routers/AnalyticsRouter.js:
--------------------------------------------------------------------------------
1 | // AnalyticsRouter.js
2 | import PromiseRouter from '../PromiseRouter';
3 |
4 | // Returns a promise that resolves to an empty object response
5 | function ignoreAndSucceed(req) {
6 | return Promise.resolve({
7 | response: {}
8 | });
9 | }
10 |
11 |
12 | export class AnalyticsRouter extends PromiseRouter {
13 | mountRoutes() {
14 | this.route('POST','/events/AppOpened', ignoreAndSucceed);
15 | this.route('POST','/events/:eventName', ignoreAndSucceed);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/Push/PushAdapter.js:
--------------------------------------------------------------------------------
1 | // Push Adapter
2 | //
3 | // Allows you to change the push notification mechanism.
4 | //
5 | // Adapter classes must implement the following functions:
6 | // * getValidPushTypes()
7 | // * send(devices, installations)
8 | //
9 | // Default is ParsePushAdapter, which uses GCM for
10 | // android push and APNS for ios push.
11 |
12 | export class PushAdapter {
13 | send(devices, installations) { }
14 |
15 | /**
16 | * Get an array of valid push types.
17 | * @returns {Array} An array of valid push types
18 | */
19 | getValidPushTypes() {}
20 | }
21 |
22 | export default PushAdapter;
23 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | # Emacs
30 | *~
31 |
32 | # WebStorm/IntelliJ
33 | .idea
34 |
--------------------------------------------------------------------------------
/src/Adapters/Files/FilesAdapter.js:
--------------------------------------------------------------------------------
1 | // Files Adapter
2 | //
3 | // Allows you to change the file storage mechanism.
4 | //
5 | // Adapter classes must implement the following functions:
6 | // * createFile(config, filename, data)
7 | // * getFileData(config, filename)
8 | // * getFileLocation(config, request, filename)
9 | //
10 | // Default is GridStoreAdapter, which requires mongo
11 | // and for the API server to be using the ExportAdapter
12 | // database adapter.
13 |
14 | export class FilesAdapter {
15 | createFile(config, filename, data) { }
16 |
17 | deleteFile(config, filename) { }
18 |
19 | getFileData(config, filename) { }
20 |
21 | getFileLocation(config, filename) { }
22 | }
23 |
24 | export default FilesAdapter;
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | # Emacs
30 | *~
31 |
32 | # WebStorm/IntelliJ
33 | .idea
34 |
35 | # Babel.js
36 | lib/
37 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### Contributing to Parse Server
2 |
3 | #### Pull Requests Welcome!
4 |
5 | We really want Parse to be yours, to see it grow and thrive in the open source community.
6 |
7 | ##### Please Do's
8 |
9 | * Take testing seriously! Aim to increase the test coverage with every pull request.
10 | * Run the tests for the file you are working on with `npm test spec/MyFile.spec.js`
11 | * Run the tests for the whole project and look at the coverage report to make sure your tests are exhaustive by running `npm test` and looking at (project-root)/lcov-report/parse-server/FileUnderTest.js.html
12 |
13 | ##### Code of Conduct
14 |
15 | This project adheres to the [Open Code of Conduct](http://todogroup.org/opencodeofconduct/#Parse Server/fjm@fb.com). By participating, you are expected to honor this code.
16 |
--------------------------------------------------------------------------------
/src/cache.js:
--------------------------------------------------------------------------------
1 | var apps = {};
2 | var stats = {};
3 | var isLoaded = false;
4 | var users = {};
5 |
6 | function getApp(app, callback) {
7 | if (apps[app]) return callback(true, apps[app]);
8 | return callback(false);
9 | }
10 |
11 | function updateStat(key, value) {
12 | stats[key] = value;
13 | }
14 |
15 | function getUser(sessionToken) {
16 | if (users[sessionToken]) return users[sessionToken];
17 | return undefined;
18 | }
19 |
20 | function setUser(sessionToken, userObject) {
21 | users[sessionToken] = userObject;
22 | }
23 |
24 | function clearUser(sessionToken) {
25 | delete users[sessionToken];
26 | }
27 |
28 | //So far used only in tests
29 | function clearCache() {
30 | apps = {};
31 | stats = {};
32 | users = {};
33 | }
34 |
35 | module.exports = {
36 | apps: apps,
37 | stats: stats,
38 | isLoaded: isLoaded,
39 | getApp: getApp,
40 | updateStat: updateStat,
41 | clearUser: clearUser,
42 | getUser: getUser,
43 | setUser: setUser,
44 | clearCache: clearCache,
45 | };
46 |
--------------------------------------------------------------------------------
/src/password.js:
--------------------------------------------------------------------------------
1 | // Tools for encrypting and decrypting passwords.
2 | // Basically promise-friendly wrappers for bcrypt.
3 | var bcrypt = require('bcrypt-nodejs');
4 |
5 | // Returns a promise for a hashed password string.
6 | function hash(password) {
7 | return new Promise(function(fulfill, reject) {
8 | bcrypt.hash(password, null, null, function(err, hashedPassword) {
9 | if (err) {
10 | reject(err);
11 | } else {
12 | fulfill(hashedPassword);
13 | }
14 | });
15 | });
16 | }
17 |
18 | // Returns a promise for whether this password compares to equal this
19 | // hashed password.
20 | function compare(password, hashedPassword) {
21 | return new Promise(function(fulfill, reject) {
22 | bcrypt.compare(password, hashedPassword, function(err, success) {
23 | if (err) {
24 | reject(err);
25 | } else {
26 | fulfill(success);
27 | }
28 | });
29 | });
30 | }
31 |
32 | module.exports = {
33 | hash: hash,
34 | compare: compare
35 | };
36 |
--------------------------------------------------------------------------------
/src/oauth/twitter.js:
--------------------------------------------------------------------------------
1 | // Helper functions for accessing the meetup API.
2 | var OAuth = require('./OAuth1Client');
3 | var Parse = require('parse/node').Parse;
4 |
5 | // Returns a promise that fulfills iff this user id is valid.
6 | function validateAuthData(authData, options) {
7 | var client = new OAuth(options);
8 | client.host = "api.twitter.com";
9 | client.auth_token = authData.auth_token;
10 | client.auth_token_secret = authData.auth_token_secret;
11 |
12 | return client.get("/1.1/account/verify_credentials.json").then((data) => {
13 | if (data && data.id == authData.id) {
14 | return;
15 | }
16 | throw new Parse.Error(
17 | Parse.Error.OBJECT_NOT_FOUND,
18 | 'Twitter auth is invalid for this user.');
19 | });
20 | }
21 |
22 | // Returns a promise that fulfills iff this app id is valid.
23 | function validateAppId() {
24 | return Promise.resolve();
25 | }
26 |
27 | module.exports = {
28 | validateAppId: validateAppId,
29 | validateAuthData: validateAuthData
30 | };
31 |
--------------------------------------------------------------------------------
/spec/FilesController.spec.js:
--------------------------------------------------------------------------------
1 | var FilesController = require('../src/Controllers/FilesController').FilesController;
2 | var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
3 | var Config = require("../src/Config");
4 |
5 | // Small additional tests to improve overall coverage
6 | describe("FilesController",()=>{
7 |
8 | it("should properly expand objects", (done) => {
9 | var config = new Config(Parse.applicationId);
10 | var adapter = new GridStoreAdapter();
11 | var filesController = new FilesController(adapter);
12 | var result = filesController.expandFilesInObject(config, function(){});
13 |
14 | expect(result).toBeUndefined();
15 |
16 | var fullFile = {
17 | type: '__type',
18 | url: "http://an.url"
19 | }
20 |
21 | var anObject = {
22 | aFile: fullFile
23 | }
24 | filesController.expandFilesInObject(config, anObject);
25 | expect(anObject.aFile.url).toEqual("http://an.url");
26 |
27 | done();
28 | })
29 | })
--------------------------------------------------------------------------------
/bin/dev:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var nodemon = require('nodemon');
4 | var babel = require("babel-core");
5 | var gaze = require('gaze');
6 | var fs = require('fs');
7 | var path = require('path');
8 |
9 | // Watch the src and transpile when changed
10 | gaze('src/**/*', function(err, watcher) {
11 | if (err) throw err;
12 | watcher.on('changed', function(sourceFile) {
13 | console.log(sourceFile + " has changed");
14 | try {
15 | targetFile = path.relative(__dirname, sourceFile).replace(/\/src\//, '/lib/');
16 | targetFile = path.resolve(__dirname, targetFile);
17 | fs.writeFile(targetFile, babel.transformFileSync(sourceFile).code);
18 | } catch (e) {
19 | console.error(e.message, e.stack);
20 | }
21 | });
22 | });
23 |
24 | try {
25 | // Run and watch dist
26 | nodemon({
27 | script: 'bin/parse-server',
28 | ext: 'js json',
29 | watch: 'lib'
30 | });
31 | } catch (e) {
32 | console.error(e.message, e.stack);
33 | }
34 |
35 | process.once('SIGINT', function() {
36 | process.exit(0);
37 | });
--------------------------------------------------------------------------------
/src/Adapters/AdapterLoader.js:
--------------------------------------------------------------------------------
1 |
2 | export function loadAdapter(options, defaultAdapter) {
3 | let adapter;
4 |
5 | // We have options and options have adapter key
6 | if (options) {
7 | // Pass an adapter as a module name, a function or an instance
8 | if (typeof options == "string" || typeof options == "function" || options.constructor != Object) {
9 | adapter = options;
10 | }
11 | if (options.adapter) {
12 | adapter = options.adapter;
13 | }
14 | }
15 |
16 | if (!adapter) {
17 | adapter = defaultAdapter;
18 | }
19 |
20 | // This is a string, require the module
21 | if (typeof adapter === "string") {
22 | adapter = require(adapter);
23 | // If it's define as a module, get the default
24 | if (adapter.default) {
25 | adapter = adapter.default;
26 | }
27 | }
28 | // From there it's either a function or an object
29 | // if it's an function, instanciate and pass the options
30 | if (typeof adapter === "function") {
31 | var Adapter = adapter;
32 | adapter = new Adapter(options);
33 | }
34 | return adapter;
35 | }
36 |
--------------------------------------------------------------------------------
/src/Adapters/Push/PushAdapterUtils.js:
--------------------------------------------------------------------------------
1 | /**g
2 | * Classify the device token of installations based on its device type.
3 | * @param {Object} installations An array of installations
4 | * @param {Array} validPushTypes An array of valid push types(string)
5 | * @returns {Object} A map whose key is device type and value is an array of device
6 | */
7 | export function classifyInstallations(installations, validPushTypes) {
8 | // Init deviceTokenMap, create a empty array for each valid pushType
9 | let deviceMap = {};
10 | for (let validPushType of validPushTypes) {
11 | deviceMap[validPushType] = [];
12 | }
13 | for (let installation of installations) {
14 | // No deviceToken, ignore
15 | if (!installation.deviceToken) {
16 | continue;
17 | }
18 | let pushType = installation.deviceType;
19 | if (deviceMap[pushType]) {
20 | deviceMap[pushType].push({
21 | deviceToken: installation.deviceToken,
22 | appIdentifier: installation.appIdentifier
23 | });
24 | } else {
25 | console.log('Unknown push type from installation %j', installation);
26 | }
27 | }
28 | return deviceMap;
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/src/Routers/RolesRouter.js:
--------------------------------------------------------------------------------
1 |
2 | import ClassesRouter from './ClassesRouter';
3 | import PromiseRouter from '../PromiseRouter';
4 | import rest from '../rest';
5 |
6 | export class RolesRouter extends ClassesRouter {
7 | handleFind(req) {
8 | req.params.className = '_Role';
9 | return super.handleFind(req);
10 | }
11 |
12 | handleGet(req) {
13 | req.params.className = '_Role';
14 | return super.handleGet(req);
15 | }
16 |
17 | handleCreate(req) {
18 | req.params.className = '_Role';
19 | return super.handleCreate(req);
20 | }
21 |
22 | handleUpdate(req) {
23 | req.params.className = '_Role';
24 | return super.handleUpdate(req);
25 | }
26 |
27 | handleDelete(req) {
28 | req.params.className = '_Role';
29 | return super.handleDelete(req);
30 | }
31 |
32 | mountRoutes() {
33 | this.route('GET','/roles', req => { return this.handleFind(req); });
34 | this.route('GET','/roles/:objectId', req => { return this.handleGet(req); });
35 | this.route('POST','/roles', req => { return this.handleCreate(req); });
36 | this.route('PUT','/roles/:objectId', req => { return this.handleUpdate(req); });
37 | this.route('DELETE','/roles/:objectId', req => { return this.handleDelete(req); });
38 | }
39 | }
40 |
41 | export default RolesRouter;
42 |
--------------------------------------------------------------------------------
/src/global_config.js:
--------------------------------------------------------------------------------
1 | // global_config.js
2 |
3 | var Parse = require('parse/node').Parse;
4 |
5 | import PromiseRouter from './PromiseRouter';
6 | var router = new PromiseRouter();
7 |
8 | function getGlobalConfig(req) {
9 | return req.config.database.rawCollection('_GlobalConfig')
10 | .then(coll => coll.findOne({'_id': 1}))
11 | .then(globalConfig => ({response: { params: globalConfig.params }}))
12 | .catch(() => ({
13 | status: 404,
14 | response: {
15 | code: Parse.Error.INVALID_KEY_NAME,
16 | error: 'config does not exist',
17 | }
18 | }));
19 | }
20 |
21 | function updateGlobalConfig(req) {
22 | if (!req.auth.isMaster) {
23 | return Promise.resolve({
24 | status: 401,
25 | response: {error: 'unauthorized'},
26 | });
27 | }
28 |
29 | return req.config.database.rawCollection('_GlobalConfig')
30 | .then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body }))
31 | .then(response => {
32 | return { response: { result: true } }
33 | })
34 | .catch(() => ({
35 | status: 404,
36 | response: {
37 | code: Parse.Error.INVALID_KEY_NAME,
38 | error: 'config cannot be updated',
39 | }
40 | }));
41 | }
42 |
43 | router.route('GET', '/config', getGlobalConfig);
44 | router.route('PUT', '/config', updateGlobalConfig);
45 |
46 | module.exports = router;
47 |
--------------------------------------------------------------------------------
/src/Config.js:
--------------------------------------------------------------------------------
1 | // A Config object provides information about how a specific app is
2 | // configured.
3 | // mount is the URL for the root of the API; includes http, domain, etc.
4 | export class Config {
5 |
6 | constructor(applicationId, mount) {
7 | var cache = require('./cache');
8 | var DatabaseAdapter = require('./DatabaseAdapter');
9 |
10 | var cacheInfo = cache.apps[applicationId];
11 | this.valid = !!cacheInfo;
12 | if (!this.valid) {
13 | return;
14 | }
15 |
16 | this.applicationId = applicationId;
17 | this.collectionPrefix = cacheInfo.collectionPrefix || '';
18 | this.masterKey = cacheInfo.masterKey;
19 | this.clientKey = cacheInfo.clientKey;
20 | this.javascriptKey = cacheInfo.javascriptKey;
21 | this.dotNetKey = cacheInfo.dotNetKey;
22 | this.restAPIKey = cacheInfo.restAPIKey;
23 | this.fileKey = cacheInfo.fileKey;
24 | this.facebookAppIds = cacheInfo.facebookAppIds;
25 | this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
26 |
27 | this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
28 | this.filesController = cacheInfo.filesController;
29 | this.pushController = cacheInfo.pushController;
30 | this.loggerController = cacheInfo.loggerController;
31 | this.oauth = cacheInfo.oauth;
32 |
33 | this.mount = mount;
34 | }
35 | };
36 |
37 | export default Config;
38 | module.exports = Config;
39 |
--------------------------------------------------------------------------------
/src/oauth/google.js:
--------------------------------------------------------------------------------
1 | // Helper functions for accessing the google API.
2 | var https = require('https');
3 | var Parse = require('parse/node').Parse;
4 |
5 | // Returns a promise that fulfills iff this user id is valid.
6 | function validateAuthData(authData) {
7 | return request("tokeninfo?access_token="+authData.access_token)
8 | .then((response) => {
9 | if (response && response.user_id == authData.id) {
10 | return;
11 | }
12 | throw new Parse.Error(
13 | Parse.Error.OBJECT_NOT_FOUND,
14 | 'Google auth is invalid for this user.');
15 | });
16 | }
17 |
18 | // Returns a promise that fulfills iff this app id is valid.
19 | function validateAppId() {
20 | return Promise.resolve();
21 | }
22 |
23 | // A promisey wrapper for api requests
24 | function request(path) {
25 | return new Promise(function(resolve, reject) {
26 | https.get("https://www.googleapis.com/oauth2/v1/" + path, function(res) {
27 | var data = '';
28 | res.on('data', function(chunk) {
29 | data += chunk;
30 | });
31 | res.on('end', function() {
32 | data = JSON.parse(data);
33 | resolve(data);
34 | });
35 | }).on('error', function(e) {
36 | reject('Failed to validate this access token with Google.');
37 | });
38 | });
39 | }
40 |
41 | module.exports = {
42 | validateAppId: validateAppId,
43 | validateAuthData: validateAuthData
44 | };
45 |
--------------------------------------------------------------------------------
/src/oauth/instagram.js:
--------------------------------------------------------------------------------
1 | // Helper functions for accessing the instagram API.
2 | var https = require('https');
3 | var Parse = require('parse/node').Parse;
4 |
5 | // Returns a promise that fulfills iff this user id is valid.
6 | function validateAuthData(authData) {
7 | return request("users/self/?access_token="+authData.access_token)
8 | .then((response) => {
9 | if (response && response.data && response.data.id == authData.id) {
10 | return;
11 | }
12 | throw new Parse.Error(
13 | Parse.Error.OBJECT_NOT_FOUND,
14 | 'Instagram auth is invalid for this user.');
15 | });
16 | }
17 |
18 | // Returns a promise that fulfills iff this app id is valid.
19 | function validateAppId() {
20 | return Promise.resolve();
21 | }
22 |
23 | // A promisey wrapper for api requests
24 | function request(path) {
25 | return new Promise(function(resolve, reject) {
26 | https.get("https://api.instagram.com/v1/" + path, function(res) {
27 | var data = '';
28 | res.on('data', function(chunk) {
29 | data += chunk;
30 | });
31 | res.on('end', function() {
32 | data = JSON.parse(data);
33 | resolve(data);
34 | });
35 | }).on('error', function(e) {
36 | reject('Failed to validate this access token with Instagram.');
37 | });
38 | });
39 | }
40 |
41 | module.exports = {
42 | validateAppId: validateAppId,
43 | validateAuthData: validateAuthData
44 | };
45 |
--------------------------------------------------------------------------------
/src/oauth/meetup.js:
--------------------------------------------------------------------------------
1 | // Helper functions for accessing the meetup API.
2 | var https = require('https');
3 | var Parse = require('parse/node').Parse;
4 |
5 | // Returns a promise that fulfills iff this user id is valid.
6 | function validateAuthData(authData) {
7 | return request('member/self', authData.access_token)
8 | .then((data) => {
9 | if (data && data.id == authData.id) {
10 | return;
11 | }
12 | throw new Parse.Error(
13 | Parse.Error.OBJECT_NOT_FOUND,
14 | 'Meetup auth is invalid for this user.');
15 | });
16 | }
17 |
18 | // Returns a promise that fulfills iff this app id is valid.
19 | function validateAppId() {
20 | return Promise.resolve();
21 | }
22 |
23 | // A promisey wrapper for api requests
24 | function request(path, access_token) {
25 | return new Promise(function(resolve, reject) {
26 | https.get({
27 | host: 'api.meetup.com',
28 | path: '/2/' + path,
29 | headers: {
30 | 'Authorization': 'bearer '+access_token
31 | }
32 | }, function(res) {
33 | var data = '';
34 | res.on('data', function(chunk) {
35 | data += chunk;
36 | });
37 | res.on('end', function() {
38 | data = JSON.parse(data);
39 | resolve(data);
40 | });
41 | }).on('error', function(e) {
42 | reject('Failed to validate this access token with Meetup.');
43 | });
44 | });
45 | }
46 |
47 | module.exports = {
48 | validateAppId: validateAppId,
49 | validateAuthData: validateAuthData
50 | };
51 |
--------------------------------------------------------------------------------
/src/oauth/github.js:
--------------------------------------------------------------------------------
1 | // Helper functions for accessing the github API.
2 | var https = require('https');
3 | var Parse = require('parse/node').Parse;
4 |
5 | // Returns a promise that fulfills iff this user id is valid.
6 | function validateAuthData(authData) {
7 | return request('user', authData.access_token)
8 | .then((data) => {
9 | if (data && data.id == authData.id) {
10 | return;
11 | }
12 | throw new Parse.Error(
13 | Parse.Error.OBJECT_NOT_FOUND,
14 | 'Github auth is invalid for this user.');
15 | });
16 | }
17 |
18 | // Returns a promise that fulfills iff this app id is valid.
19 | function validateAppId() {
20 | return Promise.resolve();
21 | }
22 |
23 | // A promisey wrapper for api requests
24 | function request(path, access_token) {
25 | return new Promise(function(resolve, reject) {
26 | https.get({
27 | host: 'api.github.com',
28 | path: '/' + path,
29 | headers: {
30 | 'Authorization': 'bearer '+access_token,
31 | 'User-Agent': 'parse-server'
32 | }
33 | }, function(res) {
34 | var data = '';
35 | res.on('data', function(chunk) {
36 | data += chunk;
37 | });
38 | res.on('end', function() {
39 | data = JSON.parse(data);
40 | resolve(data);
41 | });
42 | }).on('error', function(e) {
43 | reject('Failed to validate this access token with Github.');
44 | });
45 | });
46 | }
47 |
48 | module.exports = {
49 | validateAppId: validateAppId,
50 | validateAuthData: validateAuthData
51 | };
52 |
--------------------------------------------------------------------------------
/spec/index.spec.js:
--------------------------------------------------------------------------------
1 | var request = require('request');
2 |
3 | describe('server', () => {
4 | it('requires a master key and app id', done => {
5 | expect(setServerConfiguration.bind(undefined, { })).toThrow('You must provide an appId!');
6 | expect(setServerConfiguration.bind(undefined, { appId: 'myId' })).toThrow('You must provide a masterKey!');
7 | expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!');
8 | done();
9 | });
10 |
11 | it('fails if database is unreachable', done => {
12 | setServerConfiguration({
13 | databaseURI: 'mongodb://fake:fake@ds043605.mongolab.com:43605/drew3',
14 | serverURL: 'http://localhost:8378/1',
15 | appId: 'test',
16 | javascriptKey: 'test',
17 | dotNetKey: 'windows',
18 | clientKey: 'client',
19 | restAPIKey: 'rest',
20 | masterKey: 'test',
21 | collectionPrefix: 'test_',
22 | fileKey: 'test',
23 | });
24 | //Need to use rest api because saving via JS SDK results in fail() not getting called
25 | request.post({
26 | url: 'http://localhost:8378/1/classes/NewClass',
27 | headers: {
28 | 'X-Parse-Application-Id': 'test',
29 | 'X-Parse-REST-API-Key': 'rest',
30 | },
31 | body: {},
32 | json: true,
33 | }, (error, response, body) => {
34 | expect(response.statusCode).toEqual(500);
35 | expect(body.code).toEqual(1);
36 | expect(body.message).toEqual('Internal server error.');
37 | done();
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/src/oauth/linkedin.js:
--------------------------------------------------------------------------------
1 | // Helper functions for accessing the linkedin API.
2 | var https = require('https');
3 | var Parse = require('parse/node').Parse;
4 |
5 | // Returns a promise that fulfills iff this user id is valid.
6 | function validateAuthData(authData) {
7 | return request('people/~:(id)', authData.access_token)
8 | .then((data) => {
9 | if (data && data.id == authData.id) {
10 | return;
11 | }
12 | throw new Parse.Error(
13 | Parse.Error.OBJECT_NOT_FOUND,
14 | 'Meetup auth is invalid for this user.');
15 | });
16 | }
17 |
18 | // Returns a promise that fulfills iff this app id is valid.
19 | function validateAppId() {
20 | return Promise.resolve();
21 | }
22 |
23 | // A promisey wrapper for api requests
24 | function request(path, access_token) {
25 | return new Promise(function(resolve, reject) {
26 | https.get({
27 | host: 'api.linkedin.com',
28 | path: '/v1/' + path,
29 | headers: {
30 | 'Authorization': 'Bearer '+access_token,
31 | 'x-li-format': 'json'
32 | }
33 | }, function(res) {
34 | var data = '';
35 | res.on('data', function(chunk) {
36 | data += chunk;
37 | });
38 | res.on('end', function() {
39 | data = JSON.parse(data);
40 | resolve(data);
41 | });
42 | }).on('error', function(e) {
43 | reject('Failed to validate this access token with Linkedin.');
44 | });
45 | });
46 | }
47 |
48 | module.exports = {
49 | validateAppId: validateAppId,
50 | validateAuthData: validateAuthData
51 | };
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD License
2 |
3 | For Parse Server software
4 |
5 | Copyright (c) 2015-present, Parse, LLC. All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without modification,
8 | are permitted provided that the following conditions are met:
9 |
10 | * Redistributions of source code must retain the above copyright notice, this
11 | list of conditions and the following disclaimer.
12 |
13 | * Redistributions in binary form must reproduce the above copyright notice,
14 | this list of conditions and the following disclaimer in the documentation
15 | and/or other materials provided with the distribution.
16 |
17 | * Neither the name Parse nor the names of its contributors may be used to
18 | endorse or promote products derived from this software without specific
19 | prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
--------------------------------------------------------------------------------
/bin/parse-server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | var express = require('express');
3 | var ParseServer = require("../lib/index").ParseServer;
4 |
5 | var app = express();
6 |
7 | var options = {};
8 | if (process.env.PARSE_SERVER_OPTIONS) {
9 |
10 | options = JSON.parse(process.env.PARSE_SERVER_OPTIONS);
11 |
12 | } else {
13 |
14 | options.databaseURI = process.env.PARSE_SERVER_DATABASE_URI;
15 | options.cloud = process.env.PARSE_SERVER_CLOUD_CODE_MAIN;
16 | options.collectionPrefix = process.env.PARSE_SERVER_COLLECTION_PREFIX;
17 |
18 | // Keys and App ID
19 | options.appId = process.env.PARSE_SERVER_APPLICATION_ID;
20 | options.clientKey = process.env.PARSE_SERVER_CLIENT_KEY;
21 | options.restAPIKey = process.env.PARSE_SERVER_REST_API_KEY;
22 | options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY;
23 | options.javascriptKey = process.env.PARSE_SERVER_JAVASCRIPT_KEY;
24 | options.masterKey = process.env.PARSE_SERVER_MASTER_KEY;
25 | options.fileKey = process.env.PARSE_SERVER_FILE_KEY;
26 | // Comma separated list of facebook app ids
27 | var facebookAppIds = process.env.PARSE_SERVER_FACEBOOK_APP_IDS;
28 |
29 | if (facebookAppIds) {
30 | facebookAppIds = facebookAppIds.split(",");
31 | options.facebookAppIds = facebookAppIds;
32 | }
33 |
34 | var oauth = process.env.PARSE_SERVER_OAUTH_PROVIDERS;
35 | if (oauth) {
36 | options.oauth = JSON.parse(oauth);
37 | };
38 | }
39 |
40 | var mountPath = process.env.PARSE_SERVER_MOUNT_PATH || "/";
41 | var api = new ParseServer(options);
42 | app.use(mountPath, api);
43 |
44 | var port = process.env.PORT || 1337;
45 | app.listen(port, function() {
46 | console.log('parse-server-example running on http://localhost:'+ port + mountPath);
47 | });
48 |
--------------------------------------------------------------------------------
/src/cryptoUtils.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import { randomBytes } from 'crypto';
4 |
5 | // Returns a new random hex string of the given even size.
6 | export function randomHexString(size: number): string {
7 | if (size === 0) {
8 | throw new Error('Zero-length randomHexString is useless.');
9 | }
10 | if (size % 2 !== 0) {
11 | throw new Error('randomHexString size must be divisible by 2.')
12 | }
13 | return randomBytes(size/2).toString('hex');
14 | }
15 |
16 | // Returns a new random alphanumeric string of the given size.
17 | //
18 | // Note: to simplify implementation, the result has slight modulo bias,
19 | // because chars length of 62 doesn't divide the number of all bytes
20 | // (256) evenly. Such bias is acceptable for most cases when the output
21 | // length is long enough and doesn't need to be uniform.
22 | export function randomString(size: number): string {
23 | if (size === 0) {
24 | throw new Error('Zero-length randomString is useless.');
25 | }
26 | let chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
27 | 'abcdefghijklmnopqrstuvwxyz' +
28 | '0123456789');
29 | let objectId = '';
30 | let bytes = randomBytes(size);
31 | for (let i = 0; i < bytes.length; ++i) {
32 | objectId += chars[bytes.readUInt8(i) % chars.length];
33 | }
34 | return objectId;
35 | }
36 |
37 | // Returns a new random alphanumeric string suitable for object ID.
38 | export function newObjectId(): string {
39 | //TODO: increase length to better protect against collisions.
40 | return randomString(10);
41 | }
42 |
43 | // Returns a new random hex string suitable for secure tokens.
44 | export function newToken(): string {
45 | return randomHexString(32);
46 | }
47 |
--------------------------------------------------------------------------------
/src/Adapters/Files/GridStoreAdapter.js:
--------------------------------------------------------------------------------
1 | // GridStoreAdapter
2 | //
3 | // Stores files in Mongo using GridStore
4 | // Requires the database adapter to be based on mongoclient
5 |
6 | import { GridStore } from 'mongodb';
7 | import { FilesAdapter } from './FilesAdapter';
8 |
9 | export class GridStoreAdapter extends FilesAdapter {
10 | // For a given config object, filename, and data, store a file
11 | // Returns a promise
12 | createFile(config, filename, data) {
13 | return config.database.connect().then(() => {
14 | let gridStore = new GridStore(config.database.db, filename, 'w');
15 | return gridStore.open();
16 | }).then((gridStore) => {
17 | return gridStore.write(data);
18 | }).then((gridStore) => {
19 | return gridStore.close();
20 | });
21 | }
22 |
23 | deleteFile(config, filename) {
24 | return config.database.connect().then(() => {
25 | let gridStore = new GridStore(config.database.db, filename, 'w');
26 | return gridStore.open();
27 | }).then((gridStore) => {
28 | return gridStore.unlink();
29 | }).then((gridStore) => {
30 | return gridStore.close();
31 | });
32 | }
33 |
34 | getFileData(config, filename) {
35 | return config.database.connect().then(() => {
36 | return GridStore.exist(config.database.db, filename);
37 | }).then(() => {
38 | let gridStore = new GridStore(config.database.db, filename, 'r');
39 | return gridStore.open();
40 | }).then((gridStore) => {
41 | return gridStore.read();
42 | });
43 | }
44 |
45 | getFileLocation(config, filename) {
46 | return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
47 | }
48 | }
49 |
50 | export default GridStoreAdapter;
51 |
--------------------------------------------------------------------------------
/spec/LogsRouter.spec.js:
--------------------------------------------------------------------------------
1 | var LogsRouter = require('../src/Routers/LogsRouter').LogsRouter;
2 | var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
3 | var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
4 |
5 | const loggerController = new LoggerController(new FileLoggerAdapter());
6 |
7 | describe('LogsRouter', () => {
8 | it('can check valid master key of request', (done) => {
9 | // Make mock request
10 | var request = {
11 | auth: {
12 | isMaster: true
13 | },
14 | query: {},
15 | config: {
16 | loggerController: loggerController
17 | }
18 | };
19 |
20 | var router = new LogsRouter();
21 |
22 | expect(() => {
23 | router.handleGET(request);
24 | }).not.toThrow();
25 | done();
26 | });
27 |
28 | it('can check invalid construction of controller', (done) => {
29 | // Make mock request
30 | var request = {
31 | auth: {
32 | isMaster: true
33 | },
34 | query: {},
35 | config: {
36 | loggerController: undefined // missing controller
37 | }
38 | };
39 |
40 | var router = new LogsRouter();
41 |
42 | expect(() => {
43 | router.handleGET(request);
44 | }).toThrow();
45 | done();
46 | });
47 |
48 | it('can check invalid master key of request', (done) => {
49 | // Make mock request
50 | var request = {
51 | auth: {
52 | isMaster: false
53 | },
54 | query: {},
55 | config: {
56 | loggerController: loggerController
57 | }
58 | };
59 |
60 | var router = new LogsRouter();
61 |
62 | expect(() => {
63 | router.handleGET(request);
64 | }).toThrow();
65 | done();
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/src/Routers/FunctionsRouter.js:
--------------------------------------------------------------------------------
1 | // functions.js
2 |
3 | var express = require('express'),
4 | Parse = require('parse/node').Parse;
5 |
6 | import PromiseRouter from '../PromiseRouter';
7 |
8 | export class FunctionsRouter extends PromiseRouter {
9 |
10 | mountRoutes() {
11 | this.route('POST', '/functions/:functionName', FunctionsRouter.handleCloudFunction);
12 | }
13 |
14 | static createResponseObject(resolve, reject) {
15 | return {
16 | success: function(result) {
17 | resolve({
18 | response: {
19 | result: Parse._encode(result)
20 | }
21 | });
22 | },
23 | error: function(error) {
24 | reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error));
25 | }
26 | }
27 | }
28 |
29 | static handleCloudFunction(req) {
30 | if (Parse.Cloud.Functions[req.params.functionName]) {
31 |
32 | var request = {
33 | params: Object.assign({}, req.body, req.query),
34 | master: req.auth && req.auth.isMaster,
35 | user: req.auth && req.auth.user,
36 | installationId: req.info.installationId
37 | };
38 |
39 | if (Parse.Cloud.Validators[req.params.functionName]) {
40 | var result = Parse.Cloud.Validators[req.params.functionName](request);
41 | if (!result) {
42 | throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Validation failed.');
43 | }
44 | }
45 |
46 | return new Promise(function (resolve, reject) {
47 | var response = FunctionsRouter.createResponseObject(resolve, reject);
48 | Parse.Cloud.Functions[req.params.functionName](request, response);
49 | });
50 | } else {
51 | throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid function.');
52 | }
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/src/Controllers/AdaptableController.js:
--------------------------------------------------------------------------------
1 | /*
2 | AdaptableController.js
3 |
4 | AdaptableController is the base class for all controllers
5 | that support adapter,
6 | The super class takes care of creating the right instance for the adapter
7 | based on the parameters passed
8 |
9 | */
10 |
11 | // _adapter is private, use Symbol
12 | var _adapter = Symbol();
13 |
14 | export class AdaptableController {
15 |
16 | constructor(adapter) {
17 | this.adapter = adapter;
18 | }
19 |
20 | set adapter(adapter) {
21 | this.validateAdapter(adapter);
22 | this[_adapter] = adapter;
23 | }
24 |
25 | get adapter() {
26 | return this[_adapter];
27 | }
28 |
29 | expectedAdapterType() {
30 | throw new Error("Subclasses should implement expectedAdapterType()");
31 | }
32 |
33 | validateAdapter(adapter) {
34 |
35 | if (!adapter) {
36 | throw new Error(this.constructor.name+" requires an adapter");
37 | }
38 |
39 | let Type = this.expectedAdapterType();
40 | // Allow skipping for testing
41 | if (!Type) {
42 | return;
43 | }
44 |
45 | // Makes sure the prototype matches
46 | let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => {
47 | const adapterType = typeof adapter[key];
48 | const expectedType = typeof Type.prototype[key];
49 | if (adapterType !== expectedType) {
50 | obj[key] = {
51 | expected: expectedType,
52 | actual: adapterType
53 | }
54 | }
55 | return obj;
56 | }, {});
57 |
58 | if (Object.keys(mismatches).length > 0) {
59 | console.error(adapter, mismatches);
60 | throw new Error("Adapter prototype don't match expected prototype");
61 | }
62 | }
63 | }
64 |
65 | export default AdaptableController;
--------------------------------------------------------------------------------
/spec/FileLoggerAdapter.spec.js:
--------------------------------------------------------------------------------
1 | var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
2 | var Parse = require('parse/node').Parse;
3 | var request = require('request');
4 | var fs = require('fs');
5 |
6 | var LOGS_FOLDER = './test_logs/';
7 |
8 | var deleteFolderRecursive = function(path) {
9 | if( fs.existsSync(path) ) {
10 | fs.readdirSync(path).forEach(function(file,index){
11 | var curPath = path + "/" + file;
12 | if(fs.lstatSync(curPath).isDirectory()) { // recurse
13 | deleteFolderRecursive(curPath);
14 | } else { // delete file
15 | fs.unlinkSync(curPath);
16 | }
17 | });
18 | fs.rmdirSync(path);
19 | }
20 | };
21 |
22 | describe('info logs', () => {
23 |
24 | afterEach((done) => {
25 | deleteFolderRecursive(LOGS_FOLDER);
26 | done();
27 | });
28 |
29 | it("Verify INFO logs", (done) => {
30 | var fileLoggerAdapter = new FileLoggerAdapter({
31 | logsFolder: LOGS_FOLDER
32 | });
33 | fileLoggerAdapter.info('testing info logs', () => {
34 | fileLoggerAdapter.query({
35 | size: 1,
36 | level: 'info'
37 | }, (results) => {
38 | expect(results[0].message).toEqual('testing info logs');
39 | done();
40 | });
41 | });
42 | });
43 | });
44 |
45 | describe('error logs', () => {
46 |
47 | afterEach((done) => {
48 | deleteFolderRecursive(LOGS_FOLDER);
49 | done();
50 | });
51 |
52 | it("Verify ERROR logs", (done) => {
53 | var fileLoggerAdapter = new FileLoggerAdapter();
54 | fileLoggerAdapter.error('testing error logs', () => {
55 | fileLoggerAdapter.query({
56 | size: 1,
57 | level: 'error'
58 | }, (results) => {
59 | expect(results[0].message).toEqual('testing error logs');
60 | done();
61 | });
62 | });
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/src/DatabaseAdapter.js:
--------------------------------------------------------------------------------
1 | // Database Adapter
2 | //
3 | // Allows you to change the underlying database.
4 | //
5 | // Adapter classes must implement the following methods:
6 | // * a constructor with signature (connectionString, optionsObject)
7 | // * connect()
8 | // * loadSchema()
9 | // * create(className, object)
10 | // * find(className, query, options)
11 | // * update(className, query, update, options)
12 | // * destroy(className, query, options)
13 | // * This list is incomplete and the database process is not fully modularized.
14 | //
15 | // Default is ExportAdapter, which uses mongo.
16 |
17 | var ExportAdapter = require('./ExportAdapter');
18 |
19 | var adapter = ExportAdapter;
20 | var cache = require('./cache');
21 | var dbConnections = {};
22 | var databaseURI = 'mongodb://localhost:27017/parse';
23 | var appDatabaseURIs = {};
24 |
25 | function setAdapter(databaseAdapter) {
26 | adapter = databaseAdapter;
27 | }
28 |
29 | function setDatabaseURI(uri) {
30 | databaseURI = uri;
31 | }
32 |
33 | function setAppDatabaseURI(appId, uri) {
34 | appDatabaseURIs[appId] = uri;
35 | }
36 |
37 | //Used by tests
38 | function clearDatabaseURIs() {
39 | appDatabaseURIs = {};
40 | dbConnections = {};
41 | }
42 |
43 | function getDatabaseConnection(appId) {
44 | if (dbConnections[appId]) {
45 | return dbConnections[appId];
46 | }
47 |
48 | var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI);
49 | dbConnections[appId] = new adapter(dbURI, {
50 | collectionPrefix: cache.apps[appId]['collectionPrefix']
51 | });
52 | dbConnections[appId].connect();
53 | return dbConnections[appId];
54 | }
55 |
56 | module.exports = {
57 | dbConnections: dbConnections,
58 | getDatabaseConnection: getDatabaseConnection,
59 | setAdapter: setAdapter,
60 | setDatabaseURI: setDatabaseURI,
61 | setAppDatabaseURI: setAppDatabaseURI,
62 | clearDatabaseURIs: clearDatabaseURIs,
63 | };
64 |
--------------------------------------------------------------------------------
/src/Routers/LogsRouter.js:
--------------------------------------------------------------------------------
1 | import { Parse } from 'parse/node';
2 | import PromiseRouter from '../PromiseRouter';
3 |
4 | // only allow request with master key
5 | let enforceSecurity = (auth) => {
6 | if (!auth || !auth.isMaster) {
7 | throw new Parse.Error(
8 | Parse.Error.OPERATION_FORBIDDEN,
9 | 'Clients aren\'t allowed to perform the ' +
10 | 'get' + ' operation on logs.'
11 | );
12 | }
13 | }
14 |
15 | export class LogsRouter extends PromiseRouter {
16 |
17 | mountRoutes() {
18 | this.route('GET','/logs', (req) => {
19 | return this.handleGET(req);
20 | });
21 | }
22 |
23 | // Returns a promise for a {response} object.
24 | // query params:
25 | // level (optional) Level of logging you want to query for (info || error)
26 | // from (optional) Start time for the search. Defaults to 1 week ago.
27 | // until (optional) End time for the search. Defaults to current time.
28 | // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
29 | // size (optional) Number of rows returned by search. Defaults to 10
30 | handleGET(req) {
31 | if (!req.config || !req.config.loggerController) {
32 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
33 | 'Logger adapter is not availabe');
34 | }
35 |
36 | let promise = new Parse.Promise();
37 | let from = req.query.from;
38 | let until = req.query.until;
39 | let size = req.query.size;
40 | let order = req.query.order
41 | let level = req.query.level;
42 | enforceSecurity(req.auth);
43 |
44 | const options = {
45 | from,
46 | until,
47 | size,
48 | order,
49 | level,
50 | }
51 |
52 | return req.config.loggerController.getLogs(options).then((result) => {
53 | return Promise.resolve({
54 | response: result
55 | });
56 | })
57 | }
58 | }
59 |
60 | export default LogsRouter;
61 |
--------------------------------------------------------------------------------
/src/oauth/facebook.js:
--------------------------------------------------------------------------------
1 | // Helper functions for accessing the Facebook Graph API.
2 | var https = require('https');
3 | var Parse = require('parse/node').Parse;
4 |
5 | // Returns a promise that fulfills iff this user id is valid.
6 | function validateAuthData(authData) {
7 | return graphRequest('me?fields=id&access_token=' + authData.access_token)
8 | .then((data) => {
9 | if (data && data.id == authData.id) {
10 | return;
11 | }
12 | throw new Parse.Error(
13 | Parse.Error.OBJECT_NOT_FOUND,
14 | 'Facebook auth is invalid for this user.');
15 | });
16 | }
17 |
18 | // Returns a promise that fulfills iff this app id is valid.
19 | function validateAppId(appIds, authData) {
20 | var access_token = authData.access_token;
21 | if (!appIds.length) {
22 | throw new Parse.Error(
23 | Parse.Error.OBJECT_NOT_FOUND,
24 | 'Facebook auth is not configured.');
25 | }
26 | return graphRequest('app?access_token=' + access_token)
27 | .then((data) => {
28 | if (data && appIds.indexOf(data.id) != -1) {
29 | return;
30 | }
31 | throw new Parse.Error(
32 | Parse.Error.OBJECT_NOT_FOUND,
33 | 'Facebook auth is invalid for this user.');
34 | });
35 | }
36 |
37 | // A promisey wrapper for FB graph requests.
38 | function graphRequest(path) {
39 | return new Promise(function(resolve, reject) {
40 | https.get('https://graph.facebook.com/v2.5/' + path, function(res) {
41 | var data = '';
42 | res.on('data', function(chunk) {
43 | data += chunk;
44 | });
45 | res.on('end', function() {
46 | data = JSON.parse(data);
47 | resolve(data);
48 | });
49 | }).on('error', function(e) {
50 | reject('Failed to validate this access token with Facebook.');
51 | });
52 | });
53 | }
54 |
55 | module.exports = {
56 | validateAppId: validateAppId,
57 | validateAuthData: validateAuthData
58 | };
59 |
--------------------------------------------------------------------------------
/src/Routers/InstallationsRouter.js:
--------------------------------------------------------------------------------
1 | // InstallationsRouter.js
2 |
3 | import ClassesRouter from './ClassesRouter';
4 | import PromiseRouter from '../PromiseRouter';
5 | import rest from '../rest';
6 |
7 | export class InstallationsRouter extends ClassesRouter {
8 | handleFind(req) {
9 | var options = {};
10 | if (req.body.skip) {
11 | options.skip = Number(req.body.skip);
12 | }
13 | if (req.body.limit) {
14 | options.limit = Number(req.body.limit);
15 | }
16 | if (req.body.order) {
17 | options.order = String(req.body.order);
18 | }
19 | if (req.body.count) {
20 | options.count = true;
21 | }
22 | if (req.body.include) {
23 | options.include = String(req.body.include);
24 | }
25 |
26 | return rest.find(req.config, req.auth,
27 | '_Installation', req.body.where, options)
28 | .then((response) => {
29 | return {response: response};
30 | });
31 | }
32 |
33 | handleGet(req) {
34 | req.params.className = '_Installation';
35 | return super.handleGet(req);
36 | }
37 |
38 | handleCreate(req) {
39 | req.params.className = '_Installation';
40 | return super.handleCreate(req);
41 | }
42 |
43 | handleUpdate(req) {
44 | req.params.className = '_Installation';
45 | return super.handleUpdate(req);
46 | }
47 |
48 | handleDelete(req) {
49 | req.params.className = '_Installation';
50 | return super.handleDelete(req);
51 | }
52 |
53 | mountRoutes() {
54 | this.route('GET','/installations', req => { return this.handleFind(req); });
55 | this.route('GET','/installations/:objectId', req => { return this.handleGet(req); });
56 | this.route('POST','/installations', req => { return this.handleCreate(req); });
57 | this.route('PUT','/installations/:objectId', req => { return this.handleUpdate(req); });
58 | this.route('DELETE','/installations/:objectId', req => { return this.handleDelete(req); });
59 | }
60 | }
61 |
62 | export default InstallationsRouter;
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "parse-server",
3 | "version": "2.1.2",
4 | "description": "An express module providing a Parse-compatible API server",
5 | "main": "lib/index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/ParsePlatform/parse-server"
9 | },
10 | "license": "BSD-3-Clause",
11 | "dependencies": {
12 | "apn": "^1.7.5",
13 | "aws-sdk": "~2.2.33",
14 | "babel-polyfill": "^6.5.0",
15 | "babel-runtime": "^6.5.0",
16 | "bcrypt-nodejs": "0.0.3",
17 | "body-parser": "^1.14.2",
18 | "deepcopy": "^0.6.1",
19 | "express": "^4.13.4",
20 | "mime": "^1.3.4",
21 | "mongodb": "~2.1.0",
22 | "multer": "^1.1.0",
23 | "node-gcm": "^0.14.0",
24 | "parse": "^1.7.0",
25 | "request": "^2.65.0",
26 | "winston": "^2.1.1"
27 | },
28 | "devDependencies": {
29 | "babel-cli": "^6.5.1",
30 | "babel-core": "^6.5.1",
31 | "babel-istanbul": "^0.6.0",
32 | "babel-plugin-transform-flow-strip-types": "^6.5.0",
33 | "babel-preset-es2015": "^6.5.0",
34 | "babel-preset-stage-0": "^6.5.0",
35 | "babel-register": "^6.5.1",
36 | "codecov": "^1.0.1",
37 | "cross-env": "^1.0.7",
38 | "deep-diff": "^0.3.3",
39 | "flow-bin": "^0.22.0",
40 | "gaze": "^0.5.2",
41 | "jasmine": "^2.3.2",
42 | "mongodb-runner": "^3.1.15",
43 | "nodemon": "^1.8.1"
44 | },
45 | "scripts": {
46 | "dev": "npm run build && bin/dev",
47 | "build": "./node_modules/.bin/babel src/ -d lib/",
48 | "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start",
49 | "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",
50 | "posttest": "mongodb-runner stop",
51 | "start": "./bin/parse-server",
52 | "prepublish": "npm run build"
53 | },
54 | "engines": {
55 | "node": ">=4.3"
56 | },
57 | "bin": {
58 | "parse-server": "./bin/parse-server"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/PATENTS:
--------------------------------------------------------------------------------
1 | Additional Grant of Patent Rights Version 2
2 |
3 | "Software" means the Parse Server software distributed by Parse, LLC.
4 |
5 | Parse, LLC. ("Parse") hereby grants to each recipient of the Software
6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable
7 | (subject to the termination provision below) license under any Necessary
8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise
9 | transfer the Software. For avoidance of doubt, no license is granted under
10 | Parse’s rights in any patent claims that are infringed by (i) modifications
11 | to the Software made by you or any third party or (ii) the Software in
12 | combination with any software or other technology.
13 |
14 | The license granted hereunder will terminate, automatically and without notice,
15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate
16 | directly or indirectly, or take a direct financial interest in, any Patent
17 | Assertion: (i) against Parse or any of its subsidiaries or corporate
18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or
19 | in part from any software, technology, product or service of Parse or any of
20 | its subsidiaries or corporate affiliates, or (iii) against any party relating
21 | to the Software. Notwithstanding the foregoing, if Parse or any of its
22 | subsidiaries or corporate affiliates files a lawsuit alleging patent
23 | infringement against you in the first instance, and you respond by filing a
24 | patent infringement counterclaim in that lawsuit against that party that is
25 | unrelated to the Software, the license granted hereunder will not terminate
26 | under section (i) of this paragraph due to such counterclaim.
27 |
28 | A "Necessary Claim" is a claim of a patent owned by Parse that is
29 | necessarily infringed by the Software standing alone.
30 |
31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
32 | or contributory infringement or inducement to infringe any patent, including a
33 | cross-claim or counterclaim.
34 |
--------------------------------------------------------------------------------
/src/Controllers/FilesController.js:
--------------------------------------------------------------------------------
1 | // FilesController.js
2 | import { Parse } from 'parse/node';
3 | import { randomHexString } from '../cryptoUtils';
4 | import AdaptableController from './AdaptableController';
5 | import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
6 |
7 | export class FilesController extends AdaptableController {
8 |
9 | getFileData(config, filename) {
10 | return this.adapter.getFileData(config, filename);
11 | }
12 |
13 | createFile(config, filename, data) {
14 | filename = randomHexString(32) + '_' + filename;
15 | var location = this.adapter.getFileLocation(config, filename);
16 | return this.adapter.createFile(config, filename, data).then(() => {
17 | return Promise.resolve({
18 | url: location,
19 | name: filename
20 | });
21 | });
22 | }
23 |
24 | deleteFile(config, filename) {
25 | return this.adapter.deleteFile(config, filename);
26 | }
27 |
28 | /**
29 | * Find file references in REST-format object and adds the url key
30 | * with the current mount point and app id.
31 | * Object may be a single object or list of REST-format objects.
32 | */
33 | expandFilesInObject(config, object) {
34 | if (object instanceof Array) {
35 | object.map((obj) => this.expandFilesInObject(config, obj));
36 | return;
37 | }
38 | if (typeof object !== 'object') {
39 | return;
40 | }
41 | for (let key in object) {
42 | let fileObject = object[key];
43 | if (fileObject && fileObject['__type'] === 'File') {
44 | if (fileObject['url']) {
45 | continue;
46 | }
47 | let filename = fileObject['name'];
48 | if (filename.indexOf('tfss-') === 0) {
49 | fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename);
50 | } else {
51 | fileObject['url'] = this.adapter.getFileLocation(config, filename);
52 | }
53 | }
54 | }
55 | }
56 |
57 | expectedAdapterType() {
58 | return FilesAdapter;
59 | }
60 | }
61 |
62 | export default FilesController;
63 |
--------------------------------------------------------------------------------
/src/Adapters/Push/ParsePushAdapter.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | // ParsePushAdapter is the default implementation of
3 | // PushAdapter, it uses GCM for android push and APNS
4 | // for ios push.
5 |
6 | const Parse = require('parse/node').Parse;
7 | const GCM = require('../../GCM');
8 | const APNS = require('../../APNS');
9 | import PushAdapter from './PushAdapter';
10 | import { classifyInstallations } from './PushAdapterUtils';
11 |
12 | export class ParsePushAdapter extends PushAdapter {
13 | constructor(pushConfig = {}) {
14 | super(pushConfig);
15 | this.validPushTypes = ['ios', 'android'];
16 | this.senderMap = {};
17 | let pushTypes = Object.keys(pushConfig);
18 |
19 | for (let pushType of pushTypes) {
20 | if (this.validPushTypes.indexOf(pushType) < 0) {
21 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
22 | 'Push to ' + pushTypes + ' is not supported');
23 | }
24 | switch (pushType) {
25 | case 'ios':
26 | this.senderMap[pushType] = new APNS(pushConfig[pushType]);
27 | break;
28 | case 'android':
29 | this.senderMap[pushType] = new GCM(pushConfig[pushType]);
30 | break;
31 | }
32 | }
33 | }
34 |
35 | getValidPushTypes() {
36 | return this.validPushTypes;
37 | }
38 |
39 | static classifyInstallations(installations, validTypes) {
40 | return classifyInstallations(installations, validTypes)
41 | }
42 |
43 | send(data, installations) {
44 | let deviceMap = classifyInstallations(installations, this.validPushTypes);
45 | let sendPromises = [];
46 | for (let pushType in deviceMap) {
47 | let sender = this.senderMap[pushType];
48 | if (!sender) {
49 | console.log('Can not find sender for push type %s, %j', pushType, data);
50 | continue;
51 | }
52 | let devices = deviceMap[pushType];
53 | sendPromises.push(sender.send(data, devices));
54 | }
55 | return Parse.Promise.when(sendPromises);
56 | }
57 | }
58 |
59 | export default ParsePushAdapter;
60 | module.exports = ParsePushAdapter;
61 |
--------------------------------------------------------------------------------
/src/Routers/SessionsRouter.js:
--------------------------------------------------------------------------------
1 |
2 | import ClassesRouter from './ClassesRouter';
3 | import PromiseRouter from '../PromiseRouter';
4 | import rest from '../rest';
5 | import Auth from '../Auth';
6 |
7 | export class SessionsRouter extends ClassesRouter {
8 | handleFind(req) {
9 | req.params.className = '_Session';
10 | return super.handleFind(req);
11 | }
12 |
13 | handleGet(req) {
14 | req.params.className = '_Session';
15 | return super.handleGet(req);
16 | }
17 |
18 | handleCreate(req) {
19 | req.params.className = '_Session';
20 | return super.handleCreate(req);
21 | }
22 |
23 | handleUpdate(req) {
24 | req.params.className = '_Session';
25 | return super.handleUpdate(req);
26 | }
27 |
28 | handleDelete(req) {
29 | req.params.className = '_Session';
30 | return super.handleDelete(req);
31 | }
32 |
33 | handleMe(req) {
34 | // TODO: Verify correct behavior
35 | if (!req.info || !req.info.sessionToken) {
36 | throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
37 | 'Session token required.');
38 | }
39 | return rest.find(req.config, Auth.master(req.config), '_Session', { _session_token: req.info.sessionToken })
40 | .then((response) => {
41 | if (!response.results || response.results.length == 0) {
42 | throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
43 | 'Session token not found.');
44 | }
45 | return {
46 | response: response.results[0]
47 | };
48 | });
49 | }
50 |
51 | mountRoutes() {
52 | this.route('GET','/sessions/me', req => { return this.handleMe(req); });
53 | this.route('GET', '/sessions', req => { return this.handleFind(req); });
54 | this.route('GET', '/sessions/:objectId', req => { return this.handleGet(req); });
55 | this.route('POST', '/sessions', req => { return this.handleCreate(req); });
56 | this.route('PUT', '/sessions/:objectId', req => { return this.handleUpdate(req); });
57 | this.route('DELETE', '/sessions/:objectId', req => { return this.handleDelete(req); });
58 | }
59 | }
60 |
61 | export default SessionsRouter;
62 |
--------------------------------------------------------------------------------
/spec/ParseRole.spec.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | // Roles are not accessible without the master key, so they are not intended
4 | // for use by clients. We can manually test them using the master key.
5 |
6 | describe('Parse Role testing', () => {
7 |
8 | it('Do a bunch of basic role testing', (done) => {
9 |
10 | var user;
11 | var role;
12 |
13 | createTestUser().then((x) => {
14 | user = x;
15 | role = new Parse.Object('_Role');
16 | role.set('name', 'Foos');
17 | var users = role.relation('users');
18 | users.add(user);
19 | return role.save({}, { useMasterKey: true });
20 | }).then((x) => {
21 | var query = new Parse.Query('_Role');
22 | return query.find({ useMasterKey: true });
23 | }).then((x) => {
24 | expect(x.length).toEqual(1);
25 | var relation = x[0].relation('users').query();
26 | return relation.first({ useMasterKey: true });
27 | }).then((x) => {
28 | expect(x.id).toEqual(user.id);
29 | // Here we've got a valid role and a user assigned.
30 | // Lets create an object only the role can read/write and test
31 | // the different scenarios.
32 | var obj = new Parse.Object('TestObject');
33 | var acl = new Parse.ACL();
34 | acl.setPublicReadAccess(false);
35 | acl.setPublicWriteAccess(false);
36 | acl.setRoleReadAccess('Foos', true);
37 | acl.setRoleWriteAccess('Foos', true);
38 | obj.setACL(acl);
39 | return obj.save();
40 | }).then((x) => {
41 | var query = new Parse.Query('TestObject');
42 | return query.find({ sessionToken: user.getSessionToken() });
43 | }).then((x) => {
44 | expect(x.length).toEqual(1);
45 | var objAgain = x[0];
46 | objAgain.set('foo', 'bar');
47 | // This should succeed:
48 | return objAgain.save({}, {sessionToken: user.getSessionToken()});
49 | }).then((x) => {
50 | x.set('foo', 'baz');
51 | // This should fail:
52 | return x.save({},{sessionToken: ""});
53 | }).then((x) => {
54 | fail('Should not have been able to save.');
55 | }, (e) => {
56 | done();
57 | });
58 |
59 | });
60 |
61 | });
62 |
63 |
--------------------------------------------------------------------------------
/src/httpRequest.js:
--------------------------------------------------------------------------------
1 | var request = require("request"),
2 | Parse = require('parse/node').Parse;
3 |
4 | var encodeBody = function(body, headers = {}) {
5 | if (typeof body !== 'object') {
6 | return body;
7 | }
8 | var contentTypeKeys = Object.keys(headers).filter((key) => {
9 | return key.match(/content-type/i) != null;
10 | });
11 |
12 | if (contentTypeKeys.length == 1) {
13 | var contentType = contentTypeKeys[0];
14 | if (headers[contentType].match(/application\/json/i)) {
15 | body = JSON.stringify(body);
16 | } else if(headers[contentType].match(/application\/x-www-form-urlencoded/i)) {
17 | body = Object.keys(body).map(function(key){
18 | return `${key}=${encodeURIComponent(body[key])}`
19 | }).join("&");
20 | }
21 | }
22 | return body;
23 | }
24 |
25 | module.exports = function(options) {
26 | var promise = new Parse.Promise();
27 | var callbacks = {
28 | success: options.success,
29 | error: options.error
30 | };
31 | delete options.success;
32 | delete options.error;
33 | delete options.uri; // not supported
34 | options.body = encodeBody(options.body, options.headers);
35 | // set follow redirects to false by default
36 | options.followRedirect = options.followRedirects == true;
37 |
38 | request(options, (error, response, body) => {
39 | var httpResponse = {};
40 | httpResponse.status = response.statusCode;
41 | httpResponse.headers = response.headers;
42 | httpResponse.buffer = new Buffer(response.body);
43 | httpResponse.cookies = response.headers["set-cookie"];
44 | httpResponse.text = response.body;
45 | try {
46 | httpResponse.data = JSON.parse(response.body);
47 | } catch (e) {}
48 | // Consider <200 && >= 400 as errors
49 | if (error || httpResponse.status <200 || httpResponse.status >=400) {
50 | if (callbacks.error) {
51 | callbacks.error(httpResponse);
52 | }
53 | return promise.reject(httpResponse);
54 | } else {
55 | if (callbacks.success) {
56 | callbacks.success(httpResponse);
57 | }
58 | return promise.resolve(httpResponse);
59 | }
60 | });
61 | return promise;
62 | };
63 |
64 | module.exports.encodeBody = encodeBody;
65 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Parse Server Changelog
2 |
3 | ### 2.1.2 (2/19/2016)
4 |
5 | * Change: The S3 file adapter constructor requires a bucket name
6 | * Fix: Parse Query should throw if improperly encoded
7 | * Fix: Issue where roles were not used in some requests
8 | * Fix: serverURL will no longer default to api.parse.com/1
9 |
10 | ### 2.1.1 (2/18/2016)
11 |
12 | * Experimental: Schemas API support for DELETE operations
13 | * Fix: Session token issue fetching Users
14 | * Fix: Facebook auth validation
15 | * Fix: Invalid error when deleting missing session
16 |
17 | ### 2.1.0 (2/17/2016)
18 |
19 | * Feature: Support for additional OAuth providers
20 | * Feature: Ability to implement custom OAuth providers
21 | * Feature: Support for deleting Parse Files
22 | * Feature: Allow querying roles
23 | * Feature: Support for logs, extensible via Log Adapter
24 | * Feature: New Push Adapter for sending push notifications through OneSignal
25 | * Feature: Tighter default security for Users
26 | * Feature: Pass parameters to Cloud Code in query string
27 | * Feature: Disable anonymous users via configuration.
28 | * Experimental: Schemas API support for PUT operations
29 | * Fix: Prevent installation ID from being added to User
30 | * Fix: Becoming a user works properly with sessions
31 | * Fix: Including multiple object when some object are unavailable will get all the objects that are available
32 | * Fix: Invalid URL for Parse Files
33 | * Fix: Making a query without a limit now returns 100 results
34 | * Fix: Expose installation id in cloud code
35 | * Fix: Correct username for Anonymous users
36 | * Fix: Session token issue after fetching user
37 | * Fix: Issues during install process
38 | * Fix: Issue with Unity SDK sending _noBody
39 |
40 | ### 2.0.8 (2/11/2016)
41 |
42 | * Add: support for Android and iOS push notifications
43 | * Experimental: Cloud Code validation hooks (can mark as non-experimental after we have docs)
44 | * Experimental: support for schemas API (GET and POST only)
45 | * Experimental: support for Parse Config (GET and POST only)
46 | * Fix: Querying objects with equality constraint on array column
47 | * Fix: User logout will remove session token
48 | * Fix: Various files related bugs
49 | * Fix: Force minimum node version 4.3 due to security issues in earlier version
50 | * Performance Improvement: Improved caching
51 |
52 |
53 |
--------------------------------------------------------------------------------
/spec/AdapterLoader.spec.js:
--------------------------------------------------------------------------------
1 |
2 | var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
3 | var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
4 |
5 | describe("AdaptableController", ()=>{
6 |
7 | it("should instantiate an adapter from string in object", (done) => {
8 | var adapterPath = require('path').resolve("./spec/MockAdapter");
9 |
10 | var adapter = loadAdapter({
11 | adapter: adapterPath,
12 | key: "value",
13 | foo: "bar"
14 | });
15 |
16 | expect(adapter instanceof Object).toBe(true);
17 | expect(adapter.options.key).toBe("value");
18 | expect(adapter.options.foo).toBe("bar");
19 | done();
20 | });
21 |
22 | it("should instantiate an adapter from string", (done) => {
23 | var adapterPath = require('path').resolve("./spec/MockAdapter");
24 | var adapter = loadAdapter(adapterPath);
25 |
26 | expect(adapter instanceof Object).toBe(true);
27 | expect(adapter.options).toBe(adapterPath);
28 | done();
29 | });
30 |
31 | it("should instantiate an adapter from string that is module", (done) => {
32 | var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter");
33 | var adapter = loadAdapter({
34 | adapter: adapterPath
35 | });
36 |
37 | expect(adapter instanceof FilesAdapter).toBe(true);
38 | done();
39 | });
40 |
41 | it("should instantiate an adapter from function/Class", (done) => {
42 | var adapter = loadAdapter({
43 | adapter: FilesAdapter
44 | });
45 | expect(adapter instanceof FilesAdapter).toBe(true);
46 | done();
47 | });
48 |
49 | it("should instantiate the default adapter from Class", (done) => {
50 | var adapter = loadAdapter(null, FilesAdapter);
51 | expect(adapter instanceof FilesAdapter).toBe(true);
52 | done();
53 | });
54 |
55 | it("should use the default adapter", (done) => {
56 | var defaultAdapter = new FilesAdapter();
57 | var adapter = loadAdapter(null, defaultAdapter);
58 | expect(adapter instanceof FilesAdapter).toBe(true);
59 | done();
60 | });
61 |
62 | it("should use the provided adapter", (done) => {
63 | var originalAdapter = new FilesAdapter();
64 | var adapter = loadAdapter(originalAdapter);
65 | expect(adapter).toBe(originalAdapter);
66 | done();
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/src/Routers/PushRouter.js:
--------------------------------------------------------------------------------
1 | import PushController from '../Controllers/PushController'
2 | import PromiseRouter from '../PromiseRouter';
3 |
4 | export class PushRouter extends PromiseRouter {
5 |
6 | mountRoutes() {
7 | this.route("POST", "/push", req => { return this.handlePOST(req); });
8 | }
9 |
10 | /**
11 | * Check whether the api call has master key or not.
12 | * @param {Object} request A request object
13 | */
14 | static validateMasterKey(req) {
15 | if (req.info.masterKey !== req.config.masterKey) {
16 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
17 | 'Master key is invalid, you should only use master key to send push');
18 | }
19 | }
20 |
21 | handlePOST(req) {
22 | // TODO: move to middlewares when support for Promise middlewares
23 | PushRouter.validateMasterKey(req);
24 |
25 | const pushController = req.config.pushController;
26 | if (!pushController) {
27 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
28 | 'Push controller is not set');
29 | }
30 |
31 | var where = PushRouter.getQueryCondition(req);
32 |
33 | pushController.sendPush(req.body, where, req.config, req.auth);
34 | return Promise.resolve({
35 | response: {
36 | 'result': true
37 | }
38 | });
39 | }
40 |
41 | /**
42 | * Get query condition from the request body.
43 | * @param {Object} request A request object
44 | * @returns {Object} The query condition, the where field in a query api call
45 | */
46 | static getQueryCondition(req) {
47 | var body = req.body || {};
48 | var hasWhere = typeof body.where !== 'undefined';
49 | var hasChannels = typeof body.channels !== 'undefined';
50 |
51 | var where;
52 | if (hasWhere && hasChannels) {
53 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
54 | 'Channels and query can not be set at the same time.');
55 | } else if (hasWhere) {
56 | where = body.where;
57 | } else if (hasChannels) {
58 | where = {
59 | "channels": {
60 | "$in": body.channels
61 | }
62 | }
63 | } else {
64 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
65 | 'Channels and query should be set at least one.');
66 | }
67 | return where;
68 | }
69 |
70 | }
71 |
72 | export default PushRouter;
73 |
--------------------------------------------------------------------------------
/src/Controllers/LoggerController.js:
--------------------------------------------------------------------------------
1 | import { Parse } from 'parse/node';
2 | import PromiseRouter from '../PromiseRouter';
3 | import AdaptableController from './AdaptableController';
4 | import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
5 |
6 | const Promise = Parse.Promise;
7 | const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
8 |
9 | export const LogLevel = {
10 | INFO: 'info',
11 | ERROR: 'error'
12 | }
13 |
14 | export const LogOrder = {
15 | DESCENDING: 'desc',
16 | ASCENDING: 'asc'
17 | }
18 |
19 | export class LoggerController extends AdaptableController {
20 |
21 | // check that date input is valid
22 | static validDateTime(date) {
23 | if (!date) {
24 | return null;
25 | }
26 | date = new Date(date);
27 |
28 | if (!isNaN(date.getTime())) {
29 | return date;
30 | }
31 |
32 | return null;
33 | }
34 |
35 | static parseOptions(options = {}) {
36 | let from = LoggerController.validDateTime(options.from) ||
37 | new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY);
38 | let until = LoggerController.validDateTime(options.until) || new Date();
39 | let size = Number(options.size) || 10;
40 | let order = options.order || LogOrder.DESCENDING;
41 | let level = options.level || LogLevel.INFO;
42 |
43 | return {
44 | from,
45 | until,
46 | size,
47 | order,
48 | level,
49 | };
50 | }
51 |
52 | // Returns a promise for a {response} object.
53 | // query params:
54 | // level (optional) Level of logging you want to query for (info || error)
55 | // from (optional) Start time for the search. Defaults to 1 week ago.
56 | // until (optional) End time for the search. Defaults to current time.
57 | // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
58 | // size (optional) Number of rows returned by search. Defaults to 10
59 | getLogs(options= {}) {
60 | if (!this.adapter) {
61 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
62 | 'Logger adapter is not availabe');
63 | }
64 |
65 | let promise = new Parse.Promise();
66 |
67 | options = LoggerController.parseOptions(options);
68 |
69 | this.adapter.query(options, (result) => {
70 | promise.resolve(result);
71 | });
72 | return promise;
73 | }
74 |
75 | expectedAdapterType() {
76 | return LoggerAdapter;
77 | }
78 | }
79 |
80 | export default LoggerController;
81 |
--------------------------------------------------------------------------------
/src/testing-routes.js:
--------------------------------------------------------------------------------
1 | // testing-routes.js
2 |
3 | var express = require('express'),
4 | cache = require('./cache'),
5 | middlewares = require('./middlewares'),
6 | cryptoUtils = require('./cryptoUtils');
7 |
8 | var router = express.Router();
9 |
10 | // creates a unique app in the cache, with a collection prefix
11 | function createApp(req, res) {
12 | var appId = cryptoUtils.randomHexString(32);
13 | cache.apps[appId] = {
14 | 'collectionPrefix': appId + '_',
15 | 'masterKey': 'master'
16 | };
17 | var keys = {
18 | 'application_id': appId,
19 | 'client_key': 'unused',
20 | 'windows_key': 'unused',
21 | 'javascript_key': 'unused',
22 | 'webhook_key': 'unused',
23 | 'rest_api_key': 'unused',
24 | 'master_key': 'master'
25 | };
26 | res.status(200).send(keys);
27 | }
28 |
29 | // deletes all collections with the collectionPrefix of the app
30 | function clearApp(req, res) {
31 | if (!req.auth.isMaster) {
32 | return res.status(401).send({"error": "unauthorized"});
33 | }
34 | req.database.deleteEverything().then(() => {
35 | res.status(200).send({});
36 | });
37 | }
38 |
39 | // deletes all collections and drops the app from cache
40 | function dropApp(req, res) {
41 | if (!req.auth.isMaster) {
42 | return res.status(401).send({"error": "unauthorized"});
43 | }
44 | req.database.deleteEverything().then(() => {
45 | delete cache.apps[req.config.applicationId];
46 | res.status(200).send({});
47 | });
48 | }
49 |
50 | // Lets just return a success response and see what happens.
51 | function notImplementedYet(req, res) {
52 | res.status(200).send({});
53 | }
54 |
55 | router.post('/rest_clear_app',
56 | middlewares.handleParseHeaders, clearApp);
57 | router.post('/rest_block',
58 | middlewares.handleParseHeaders, notImplementedYet);
59 | router.post('/rest_mock_v8_client',
60 | middlewares.handleParseHeaders, notImplementedYet);
61 | router.post('/rest_unmock_v8_client',
62 | middlewares.handleParseHeaders, notImplementedYet);
63 | router.post('/rest_verify_analytics',
64 | middlewares.handleParseHeaders, notImplementedYet);
65 | router.post('/rest_create_app', createApp);
66 | router.post('/rest_drop_app',
67 | middlewares.handleParseHeaders, dropApp);
68 | router.post('/rest_configure_app',
69 | middlewares.handleParseHeaders, notImplementedYet);
70 |
71 | module.exports = {
72 | router: router
73 | };
74 |
--------------------------------------------------------------------------------
/src/batch.js:
--------------------------------------------------------------------------------
1 | var Parse = require('parse/node').Parse;
2 |
3 | // These methods handle batch requests.
4 | var batchPath = '/batch';
5 |
6 | // Mounts a batch-handler onto a PromiseRouter.
7 | function mountOnto(router) {
8 | router.route('POST', batchPath, (req) => {
9 | return handleBatch(router, req);
10 | });
11 | }
12 |
13 | // Returns a promise for a {response} object.
14 | // TODO: pass along auth correctly
15 | function handleBatch(router, req) {
16 | if (!req.body.requests instanceof Array) {
17 | throw new Parse.Error(Parse.Error.INVALID_JSON,
18 | 'requests must be an array');
19 | }
20 |
21 | // The batch paths are all from the root of our domain.
22 | // That means they include the API prefix, that the API is mounted
23 | // to. However, our promise router does not route the api prefix. So
24 | // we need to figure out the API prefix, so that we can strip it
25 | // from all the subrequests.
26 | if (!req.originalUrl.endsWith(batchPath)) {
27 | throw 'internal routing problem - expected url to end with batch';
28 | }
29 | var apiPrefixLength = req.originalUrl.length - batchPath.length;
30 | var apiPrefix = req.originalUrl.slice(0, apiPrefixLength);
31 |
32 | var promises = [];
33 | for (var restRequest of req.body.requests) {
34 | // The routablePath is the path minus the api prefix
35 | if (restRequest.path.slice(0, apiPrefixLength) != apiPrefix) {
36 | throw new Parse.Error(
37 | Parse.Error.INVALID_JSON,
38 | 'cannot route batch path ' + restRequest.path);
39 | }
40 | var routablePath = restRequest.path.slice(apiPrefixLength);
41 |
42 | // Use the router to figure out what handler to use
43 | var match = router.match(restRequest.method, routablePath);
44 | if (!match) {
45 | throw new Parse.Error(
46 | Parse.Error.INVALID_JSON,
47 | 'cannot route ' + restRequest.method + ' ' + routablePath);
48 | }
49 |
50 | // Construct a request that we can send to a handler
51 | var request = {
52 | body: restRequest.body,
53 | params: match.params,
54 | config: req.config,
55 | auth: req.auth
56 | };
57 |
58 | promises.push(match.handler(request).then((response) => {
59 | return {success: response.response};
60 | }, (error) => {
61 | return {error: {code: error.code, error: error.message}};
62 | }));
63 | }
64 |
65 | return Promise.all(promises).then((results) => {
66 | return {response: results};
67 | });
68 | }
69 |
70 | module.exports = {
71 | mountOnto: mountOnto
72 | };
73 |
--------------------------------------------------------------------------------
/spec/cryptoUtils.spec.js:
--------------------------------------------------------------------------------
1 | var cryptoUtils = require('../src/cryptoUtils');
2 |
3 | function givesUniqueResults(fn, iterations) {
4 | var results = {};
5 | for (var i = 0; i < iterations; i++) {
6 | var s = fn();
7 | if (results[s]) {
8 | return false;
9 | }
10 | results[s] = true;
11 | }
12 | return true;
13 | }
14 |
15 | describe('randomString', () => {
16 | it('returns a string', () => {
17 | expect(typeof cryptoUtils.randomString(10)).toBe('string');
18 | });
19 |
20 | it('returns result of the given length', () => {
21 | expect(cryptoUtils.randomString(11).length).toBe(11);
22 | expect(cryptoUtils.randomString(25).length).toBe(25);
23 | });
24 |
25 | it('throws if requested length is zero', () => {
26 | expect(() => cryptoUtils.randomString(0)).toThrow();
27 | });
28 |
29 | it('returns unique results', () => {
30 | expect(givesUniqueResults(() => cryptoUtils.randomString(10), 100)).toBe(true);
31 | });
32 | });
33 |
34 | describe('randomHexString', () => {
35 | it('returns a string', () => {
36 | expect(typeof cryptoUtils.randomHexString(10)).toBe('string');
37 | });
38 |
39 | it('returns result of the given length', () => {
40 | expect(cryptoUtils.randomHexString(10).length).toBe(10);
41 | expect(cryptoUtils.randomHexString(32).length).toBe(32);
42 | });
43 |
44 | it('throws if requested length is zero', () => {
45 | expect(() => cryptoUtils.randomHexString(0)).toThrow();
46 | });
47 |
48 | it('throws if requested length is not even', () => {
49 | expect(() => cryptoUtils.randomHexString(11)).toThrow();
50 | });
51 |
52 | it('returns unique results', () => {
53 | expect(givesUniqueResults(() => cryptoUtils.randomHexString(20), 100)).toBe(true);
54 | });
55 | });
56 |
57 | describe('newObjectId', () => {
58 | it('returns a string', () => {
59 | expect(typeof cryptoUtils.newObjectId()).toBe('string');
60 | });
61 |
62 | it('returns result with at least 10 characters', () => {
63 | expect(cryptoUtils.newObjectId().length).toBeGreaterThan(9);
64 | });
65 |
66 | it('returns unique results', () => {
67 | expect(givesUniqueResults(() => cryptoUtils.newObjectId(), 100)).toBe(true);
68 | });
69 | });
70 |
71 | describe('newToken', () => {
72 | it('returns a string', () => {
73 | expect(typeof cryptoUtils.newToken()).toBe('string');
74 | });
75 |
76 | it('returns result with at least 32 characters', () => {
77 | expect(cryptoUtils.newToken().length).toBeGreaterThan(31);
78 | });
79 |
80 | it('returns unique results', () => {
81 | expect(givesUniqueResults(() => cryptoUtils.newToken(), 100)).toBe(true);
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/spec/ParseGlobalConfig.spec.js:
--------------------------------------------------------------------------------
1 |
2 | var request = require('request');
3 | var Parse = require('parse/node').Parse;
4 | var DatabaseAdapter = require('../src/DatabaseAdapter');
5 |
6 | var database = DatabaseAdapter.getDatabaseConnection('test');
7 |
8 | describe('a GlobalConfig', () => {
9 | beforeEach(function(done) {
10 | database.rawCollection('_GlobalConfig')
11 | .then(coll => coll.updateOne({ '_id': 1}, { $set: { params: { companies: ['US', 'DK'] } } }, { upsert: true }))
12 | .then(done());
13 | });
14 |
15 | it('can be retrieved', (done) => {
16 | request.get({
17 | url: 'http://localhost:8378/1/config',
18 | json: true,
19 | headers: {
20 | 'X-Parse-Application-Id': 'test',
21 | 'X-Parse-Master-Key': 'test',
22 | },
23 | }, (error, response, body) => {
24 | expect(response.statusCode).toEqual(200);
25 | expect(body.params.companies).toEqual(['US', 'DK']);
26 | done();
27 | });
28 | });
29 |
30 | it('can be updated when a master key exists', (done) => {
31 | request.put({
32 | url: 'http://localhost:8378/1/config',
33 | json: true,
34 | body: { params: { companies: ['US', 'DK', 'SE'] } },
35 | headers: {
36 | 'X-Parse-Application-Id': 'test',
37 | 'X-Parse-Master-Key': 'test'
38 | },
39 | }, (error, response, body) => {
40 | expect(response.statusCode).toEqual(200);
41 | expect(body.result).toEqual(true);
42 | done();
43 | });
44 | });
45 |
46 | it('fail to update if master key is missing', (done) => {
47 | request.put({
48 | url: 'http://localhost:8378/1/config',
49 | json: true,
50 | body: { params: { companies: [] } },
51 | headers: {
52 | 'X-Parse-Application-Id': 'test',
53 | 'X-Parse-REST-API-Key': 'rest'
54 | },
55 | }, (error, response, body) => {
56 | expect(response.statusCode).toEqual(401);
57 | expect(body.error).toEqual('unauthorized');
58 | done();
59 | });
60 | });
61 |
62 | it('failed getting config when it is missing', (done) => {
63 | database.rawCollection('_GlobalConfig')
64 | .then(coll => coll.deleteOne({ '_id': 1}, {}, {}))
65 | .then(_ => {
66 | request.get({
67 | url: 'http://localhost:8378/1/config',
68 | json: true,
69 | headers: {
70 | 'X-Parse-Application-Id': 'test',
71 | 'X-Parse-Master-Key': 'test',
72 | },
73 | }, (error, response, body) => {
74 | expect(response.statusCode).toEqual(404);
75 | expect(body.code).toEqual(Parse.Error.INVALID_KEY_NAME);
76 | done();
77 | });
78 | });
79 | });
80 |
81 | });
82 |
--------------------------------------------------------------------------------
/spec/LoggerController.spec.js:
--------------------------------------------------------------------------------
1 | var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
2 | var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
3 |
4 | describe('LoggerController', () => {
5 | it('can check process a query witout throwing', (done) => {
6 | // Make mock request
7 | var query = {};
8 |
9 | var loggerController = new LoggerController(new FileLoggerAdapter());
10 |
11 | expect(() => {
12 | loggerController.getLogs(query).then(function(res) {
13 | expect(res.length).toBe(0);
14 | done();
15 | })
16 | }).not.toThrow();
17 | });
18 |
19 | it('properly validates dateTimes', (done) => {
20 | expect(LoggerController.validDateTime()).toBe(null);
21 | expect(LoggerController.validDateTime("String")).toBe(null);
22 | expect(LoggerController.validDateTime(123456).getTime()).toBe(123456);
23 | expect(LoggerController.validDateTime("2016-01-01Z00:00:00").getTime()).toBe(1451606400000);
24 | done();
25 | });
26 |
27 | it('can set the proper default values', (done) => {
28 | // Make mock request
29 | var result = LoggerController.parseOptions();
30 | expect(result.size).toEqual(10);
31 | expect(result.order).toEqual('desc');
32 | expect(result.level).toEqual('info');
33 |
34 | done();
35 | });
36 |
37 | it('can process a query witout throwing', (done) => {
38 | // Make mock request
39 | var query = {
40 | from: "2016-01-01Z00:00:00",
41 | until: "2016-01-01Z00:00:00",
42 | size: 5,
43 | order: 'asc',
44 | level: 'error'
45 | };
46 |
47 | var result = LoggerController.parseOptions(query);
48 |
49 | expect(result.from.getTime()).toEqual(1451606400000);
50 | expect(result.until.getTime()).toEqual(1451606400000);
51 | expect(result.size).toEqual(5);
52 | expect(result.order).toEqual('asc');
53 | expect(result.level).toEqual('error');
54 |
55 | done();
56 | });
57 |
58 | it('can check process a query witout throwing', (done) => {
59 | // Make mock request
60 | var query = {
61 | from: "2015-01-01",
62 | until: "2016-01-01",
63 | size: 5,
64 | order: 'desc',
65 | level: 'error'
66 | };
67 |
68 | var loggerController = new LoggerController(new FileLoggerAdapter());
69 |
70 | expect(() => {
71 | loggerController.getLogs(query).then(function(res) {
72 | expect(res.length).toBe(0);
73 | done();
74 | })
75 | }).not.toThrow();
76 | });
77 |
78 | it('should throw without an adapter', (done) => {
79 |
80 |
81 | expect(() => {
82 | var loggerController = new LoggerController();
83 | }).toThrow();
84 | done();
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/spec/cloud/main.js:
--------------------------------------------------------------------------------
1 | var Parse = require('parse/node').Parse;
2 |
3 | Parse.Cloud.define('hello', function(req, res) {
4 | res.success('Hello world!');
5 | });
6 |
7 | Parse.Cloud.beforeSave('BeforeSaveFail', function(req, res) {
8 | res.error('You shall not pass!');
9 | });
10 |
11 | Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function (req, res) {
12 | var query = new Parse.Query('Yolo');
13 | query.find().then(() => {
14 | res.error('Nope');
15 | }, () => {
16 | res.success();
17 | });
18 | });
19 |
20 | Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) {
21 | res.success();
22 | });
23 |
24 | Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
25 | req.object.set('foo', 'baz');
26 | res.success();
27 | });
28 |
29 | Parse.Cloud.afterSave('AfterSaveTest', function(req) {
30 | var obj = new Parse.Object('AfterSaveProof');
31 | obj.set('proof', req.object.id);
32 | obj.save();
33 | });
34 |
35 | Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) {
36 | res.error('Nope');
37 | });
38 |
39 | Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function (req, res) {
40 | var query = new Parse.Query('Yolo');
41 | query.find().then(() => {
42 | res.error('Nope');
43 | }, () => {
44 | res.success();
45 | });
46 | });
47 |
48 | Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) {
49 | res.success();
50 | });
51 |
52 | Parse.Cloud.afterDelete('AfterDeleteTest', function(req) {
53 | var obj = new Parse.Object('AfterDeleteProof');
54 | obj.set('proof', req.object.id);
55 | obj.save();
56 | });
57 |
58 | Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) {
59 | if (req.user && req.user.id) {
60 | res.success();
61 | } else {
62 | res.error('No user present on request object for beforeSave.');
63 | }
64 | });
65 |
66 | Parse.Cloud.afterSave('SaveTriggerUser', function(req) {
67 | if (!req.user || !req.user.id) {
68 | console.log('No user present on request object for afterSave.');
69 | }
70 | });
71 |
72 | Parse.Cloud.define('foo', function(req, res) {
73 | res.success({
74 | object: {
75 | __type: 'Object',
76 | className: 'Foo',
77 | objectId: '123',
78 | x: 2,
79 | relation: {
80 | __type: 'Object',
81 | className: 'Bar',
82 | objectId: '234',
83 | x: 3
84 | }
85 | },
86 | array: [{
87 | __type: 'Object',
88 | className: 'Bar',
89 | objectId: '345',
90 | x: 2
91 | }],
92 | a: 2
93 | });
94 | });
95 |
96 | Parse.Cloud.define('bar', function(req, res) {
97 | res.error('baz');
98 | });
99 |
100 | Parse.Cloud.define('requiredParameterCheck', function(req, res) {
101 | res.success();
102 | }, function(params) {
103 | return params.name;
104 | });
105 |
--------------------------------------------------------------------------------
/src/Adapters/Files/S3Adapter.js:
--------------------------------------------------------------------------------
1 | // S3Adapter
2 | //
3 | // Stores Parse files in AWS S3.
4 |
5 | import * as AWS from 'aws-sdk';
6 | import { FilesAdapter } from './FilesAdapter';
7 |
8 | const DEFAULT_S3_REGION = "us-east-1";
9 |
10 | export class S3Adapter extends FilesAdapter {
11 | // Creates an S3 session.
12 | // Providing AWS access and secret keys is mandatory
13 | // Region and bucket will use sane defaults if omitted
14 | constructor(
15 | accessKey,
16 | secretKey,
17 | bucket,
18 | { region = DEFAULT_S3_REGION,
19 | bucketPrefix = '',
20 | directAccess = false } = {}
21 | ) {
22 | super();
23 |
24 | this._region = region;
25 | this._bucket = bucket;
26 | this._bucketPrefix = bucketPrefix;
27 | this._directAccess = directAccess;
28 |
29 | let s3Options = {
30 | accessKeyId: accessKey,
31 | secretAccessKey: secretKey,
32 | params: { Bucket: this._bucket }
33 | };
34 | AWS.config._region = this._region;
35 | this._s3Client = new AWS.S3(s3Options);
36 | }
37 |
38 | // For a given config object, filename, and data, store a file in S3
39 | // Returns a promise containing the S3 object creation response
40 | createFile(config, filename, data) {
41 | let params = {
42 | Key: this._bucketPrefix + filename,
43 | Body: data
44 | };
45 | if (this._directAccess) {
46 | params.ACL = "public-read"
47 | }
48 | return new Promise((resolve, reject) => {
49 | this._s3Client.upload(params, (err, data) => {
50 | if (err !== null) {
51 | return reject(err);
52 | }
53 | resolve(data);
54 | });
55 | });
56 | }
57 |
58 | deleteFile(config, filename) {
59 | return new Promise((resolve, reject) => {
60 | let params = {
61 | Key: this._bucketPrefix + filename
62 | };
63 | this._s3Client.deleteObject(params, (err, data) =>{
64 | if(err !== null) {
65 | return reject(err);
66 | }
67 | resolve(data);
68 | });
69 | });
70 | }
71 |
72 | // Search for and return a file if found by filename
73 | // Returns a promise that succeeds with the buffer result from S3
74 | getFileData(config, filename) {
75 | let params = {Key: this._bucketPrefix + filename};
76 | return new Promise((resolve, reject) => {
77 | this._s3Client.getObject(params, (err, data) => {
78 | if (err !== null) {
79 | return reject(err);
80 | }
81 | resolve(data.Body);
82 | });
83 | });
84 | }
85 |
86 | // Generates and returns the location of a file stored in S3 for the given request and filename
87 | // The location is the direct S3 link if the option is set, otherwise we serve the file through parse-server
88 | getFileLocation(config, filename) {
89 | if (this._directAccess) {
90 | return `https://${this._bucket}.s3.amazonaws.com/${this._bucketPrefix + filename}`;
91 | }
92 | return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
93 | }
94 | }
95 |
96 | export default S3Adapter;
97 |
--------------------------------------------------------------------------------
/spec/AdaptableController.spec.js:
--------------------------------------------------------------------------------
1 |
2 | var AdaptableController = require("../src/Controllers/AdaptableController").AdaptableController;
3 | var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
4 | var FilesController = require("../src/Controllers/FilesController").FilesController;
5 |
6 | var MockController = function(options) {
7 | AdaptableController.call(this, options);
8 | }
9 | MockController.prototype = Object.create(AdaptableController.prototype);
10 | MockController.prototype.constructor = AdaptableController;
11 |
12 | describe("AdaptableController", ()=>{
13 |
14 | it("should use the provided adapter", (done) => {
15 | var adapter = new FilesAdapter();
16 | var controller = new FilesController(adapter);
17 | expect(controller.adapter).toBe(adapter);
18 | // make sure _adapter is private
19 | expect(controller._adapter).toBe(undefined);
20 | // Override _adapter is not doing anything
21 | controller._adapter = "Hello";
22 | expect(controller.adapter).toBe(adapter);
23 | done();
24 | });
25 |
26 | it("should throw when creating a new mock controller", (done) => {
27 | var adapter = new FilesAdapter();
28 | expect(() => {
29 | new MockController(adapter);
30 | }).toThrow();
31 | done();
32 | });
33 |
34 | it("should fail setting the wrong adapter to the controller", (done) => {
35 | function WrongAdapter() {};
36 | var adapter = new FilesAdapter();
37 | var controller = new FilesController(adapter);
38 | var otherAdapter = new WrongAdapter();
39 | expect(() => {
40 | controller.adapter = otherAdapter;
41 | }).toThrow();
42 | done();
43 | });
44 |
45 | it("should fail to instantiate a controller with wrong adapter", (done) => {
46 | function WrongAdapter() {};
47 | var adapter = new WrongAdapter();
48 | expect(() => {
49 | new FilesController(adapter);
50 | }).toThrow();
51 | done();
52 | });
53 |
54 | it("should fail to instantiate a controller without an adapter", (done) => {
55 | expect(() => {
56 | new FilesController();
57 | }).toThrow();
58 | done();
59 | });
60 |
61 | it("should accept an object adapter", (done) => {
62 | var adapter = {
63 | createFile: function(config, filename, data) { },
64 | deleteFile: function(config, filename) { },
65 | getFileData: function(config, filename) { },
66 | getFileLocation: function(config, filename) { },
67 | }
68 | expect(() => {
69 | new FilesController(adapter);
70 | }).not.toThrow();
71 | done();
72 | });
73 |
74 | it("should accept an object adapter", (done) => {
75 | function AGoodAdapter() {};
76 | AGoodAdapter.prototype.createFile = function(config, filename, data) { };
77 | AGoodAdapter.prototype.deleteFile = function(config, filename) { };
78 | AGoodAdapter.prototype.getFileData = function(config, filename) { };
79 | AGoodAdapter.prototype.getFileLocation = function(config, filename) { };
80 |
81 | var adapter = new AGoodAdapter();
82 | expect(() => {
83 | new FilesController(adapter);
84 | }).not.toThrow();
85 | done();
86 | });
87 | });
--------------------------------------------------------------------------------
/spec/PushRouter.spec.js:
--------------------------------------------------------------------------------
1 | var PushRouter = require('../src/Routers/PushRouter').PushRouter;
2 | var request = require('request');
3 |
4 | describe('PushRouter', () => {
5 | it('can check valid master key of request', (done) => {
6 | // Make mock request
7 | var request = {
8 | info: {
9 | masterKey: 'masterKey'
10 | },
11 | config: {
12 | masterKey: 'masterKey'
13 | }
14 | }
15 |
16 | expect(() => {
17 | PushRouter.validateMasterKey(request);
18 | }).not.toThrow();
19 | done();
20 | });
21 |
22 | it('can check invalid master key of request', (done) => {
23 | // Make mock request
24 | var request = {
25 | info: {
26 | masterKey: 'masterKey'
27 | },
28 | config: {
29 | masterKey: 'masterKeyAgain'
30 | }
31 | }
32 |
33 | expect(() => {
34 | PushRouter.validateMasterKey(request);
35 | }).toThrow();
36 | done();
37 | });
38 |
39 | it('can get query condition when channels is set', (done) => {
40 | // Make mock request
41 | var request = {
42 | body: {
43 | channels: ['Giants', 'Mets']
44 | }
45 | }
46 |
47 | var where = PushRouter.getQueryCondition(request);
48 | expect(where).toEqual({
49 | 'channels': {
50 | '$in': ['Giants', 'Mets']
51 | }
52 | });
53 | done();
54 | });
55 |
56 | it('can get query condition when where is set', (done) => {
57 | // Make mock request
58 | var request = {
59 | body: {
60 | 'where': {
61 | 'injuryReports': true
62 | }
63 | }
64 | }
65 |
66 | var where = PushRouter.getQueryCondition(request);
67 | expect(where).toEqual({
68 | 'injuryReports': true
69 | });
70 | done();
71 | });
72 |
73 | it('can get query condition when nothing is set', (done) => {
74 | // Make mock request
75 | var request = {
76 | body: {
77 | }
78 | }
79 |
80 | expect(function() {
81 | PushRouter.getQueryCondition(request);
82 | }).toThrow();
83 | done();
84 | });
85 |
86 | it('can throw on getQueryCondition when channels and where are set', (done) => {
87 | // Make mock request
88 | var request = {
89 | body: {
90 | 'channels': {
91 | '$in': ['Giants', 'Mets']
92 | },
93 | 'where': {
94 | 'injuryReports': true
95 | }
96 | }
97 | }
98 |
99 | expect(function() {
100 | PushRouter.getQueryCondition(request);
101 | }).toThrow();
102 | done();
103 | });
104 |
105 | it('sends a push through REST', (done) => {
106 | request.post({
107 | url: Parse.serverURL+"/push",
108 | json: true,
109 | body: {
110 | 'channels': {
111 | '$in': ['Giants', 'Mets']
112 | }
113 | },
114 | headers: {
115 | 'X-Parse-Application-Id': Parse.applicationId,
116 | 'X-Parse-Master-Key': Parse.masterKey
117 | }
118 | }, function(err, res, body){
119 | expect(body.result).toBe(true);
120 | done();
121 | });
122 | });
123 | });
--------------------------------------------------------------------------------
/src/triggers.js:
--------------------------------------------------------------------------------
1 | // triggers.js
2 |
3 | var Parse = require('parse/node').Parse;
4 |
5 | var Types = {
6 | beforeSave: 'beforeSave',
7 | afterSave: 'afterSave',
8 | beforeDelete: 'beforeDelete',
9 | afterDelete: 'afterDelete'
10 | };
11 |
12 | var getTrigger = function(className, triggerType) {
13 | if (Parse.Cloud.Triggers
14 | && Parse.Cloud.Triggers[triggerType]
15 | && Parse.Cloud.Triggers[triggerType][className]) {
16 | return Parse.Cloud.Triggers[triggerType][className];
17 | }
18 | return undefined;
19 | };
20 |
21 | var getRequestObject = function(triggerType, auth, parseObject, originalParseObject) {
22 | var request = {
23 | triggerName: triggerType,
24 | object: parseObject,
25 | master: false
26 | };
27 | if (originalParseObject) {
28 | request.original = originalParseObject;
29 | }
30 | if (!auth) {
31 | return request;
32 | }
33 | if (auth.isMaster) {
34 | request['master'] = true;
35 | }
36 | if (auth.user) {
37 | request['user'] = auth.user;
38 | }
39 | // TODO: Add installation to Auth?
40 | if (auth.installationId) {
41 | request['installationId'] = auth.installationId;
42 | }
43 | return request;
44 | };
45 |
46 | // Creates the response object, and uses the request object to pass data
47 | // The API will call this with REST API formatted objects, this will
48 | // transform them to Parse.Object instances expected by Cloud Code.
49 | // Any changes made to the object in a beforeSave will be included.
50 | var getResponseObject = function(request, resolve, reject) {
51 | return {
52 | success: function() {
53 | var response = {};
54 | if (request.triggerName === Types.beforeSave) {
55 | response['object'] = request.object.toJSON();
56 | }
57 | return resolve(response);
58 | },
59 | error: function(error) {
60 | var scriptError = new Parse.Error(Parse.Error.SCRIPT_FAILED, error);
61 | return reject(scriptError);
62 | }
63 | }
64 | };
65 |
66 | // To be used as part of the promise chain when saving/deleting an object
67 | // Will resolve successfully if no trigger is configured
68 | // Resolves to an object, empty or containing an object key. A beforeSave
69 | // trigger will set the object key to the rest format object to save.
70 | // originalParseObject is optional, we only need that for befote/afterSave functions
71 | var maybeRunTrigger = function(triggerType, auth, parseObject, originalParseObject) {
72 | if (!parseObject) {
73 | return Promise.resolve({});
74 | }
75 | return new Promise(function (resolve, reject) {
76 | var trigger = getTrigger(parseObject.className, triggerType);
77 | if (!trigger) return resolve({});
78 | var request = getRequestObject(triggerType, auth, parseObject, originalParseObject);
79 | var response = getResponseObject(request, resolve, reject);
80 | trigger(request, response);
81 | });
82 | };
83 |
84 | // Converts a REST-format object to a Parse.Object
85 | // data is either className or an object
86 | function inflate(data, restObject) {
87 | var copy = typeof data == 'object' ? data : {className: data};
88 | for (var key in restObject) {
89 | copy[key] = restObject[key];
90 | }
91 | return Parse.Object.fromJSON(copy);
92 | }
93 |
94 | module.exports = {
95 | getTrigger: getTrigger,
96 | getRequestObject: getRequestObject,
97 | inflate: inflate,
98 | maybeRunTrigger: maybeRunTrigger,
99 | Types: Types
100 | };
101 |
--------------------------------------------------------------------------------
/src/Routers/FilesRouter.js:
--------------------------------------------------------------------------------
1 | import PromiseRouter from '../PromiseRouter';
2 | import express from 'express';
3 | import BodyParser from 'body-parser';
4 | import * as Middlewares from '../middlewares';
5 | import { randomHexString } from '../cryptoUtils';
6 | import mime from 'mime';
7 | import Config from '../Config';
8 |
9 | export class FilesRouter {
10 |
11 | getExpressRouter(options = {}) {
12 | var router = express.Router();
13 | router.get('/files/:appId/:filename', this.getHandler);
14 |
15 | router.post('/files', function(req, res, next) {
16 | next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
17 | 'Filename not provided.'));
18 | });
19 |
20 | router.post('/files/:filename',
21 | Middlewares.allowCrossDomain,
22 | BodyParser.raw({type: () => { return true; }, limit: options.maxUploadSize || '20mb'}), // Allow uploads without Content-Type, or with any Content-Type.
23 | Middlewares.handleParseHeaders,
24 | this.createHandler
25 | );
26 |
27 | router.delete('/files/:filename',
28 | Middlewares.allowCrossDomain,
29 | Middlewares.handleParseHeaders,
30 | Middlewares.enforceMasterKeyAccess,
31 | this.deleteHandler
32 | );
33 | return router;
34 | }
35 |
36 | getHandler(req, res) {
37 | const config = new Config(req.params.appId);
38 | const filesController = config.filesController;
39 | const filename = req.params.filename;
40 | filesController.getFileData(config, filename).then((data) => {
41 | res.status(200);
42 | var contentType = mime.lookup(filename);
43 | res.set('Content-Type', contentType);
44 | res.end(data);
45 | }).catch(() => {
46 | res.status(404);
47 | res.set('Content-Type', 'text/plain');
48 | res.end('File not found.');
49 | });
50 | }
51 |
52 | createHandler(req, res, next) {
53 | if (!req.body || !req.body.length) {
54 | next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
55 | 'Invalid file upload.'));
56 | return;
57 | }
58 |
59 | if (req.params.filename.length > 128) {
60 | next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
61 | 'Filename too long.'));
62 | return;
63 | }
64 |
65 | if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
66 | next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
67 | 'Filename contains invalid characters.'));
68 | return;
69 | }
70 | let extension = '';
71 |
72 | // Not very safe there.
73 | const hasExtension = req.params.filename.indexOf('.') > 0;
74 | const contentType = req.get('Content-type');
75 | if (!hasExtension && contentType && mime.extension(contentType)) {
76 | extension = '.' + mime.extension(contentType);
77 | }
78 |
79 | const filename = req.params.filename + extension;
80 | const config = req.config;
81 | const filesController = config.filesController;
82 |
83 | filesController.createFile(config, filename, req.body).then((result) => {
84 | res.status(201);
85 | res.set('Location', result.url);
86 | res.json(result);
87 | }).catch((err) => {
88 | next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
89 | 'Could not store file.'));
90 | });
91 | }
92 |
93 | deleteHandler(req, res, next) {
94 | const filesController = req.config.filesController;
95 | filesController.deleteFile(req.config, req.params.filename).then(() => {
96 | res.status(200);
97 | // TODO: return useful JSON here?
98 | res.end();
99 | }).catch((error) => {
100 | next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR,
101 | 'Could not delete file.'));
102 | });
103 | }
104 | }
--------------------------------------------------------------------------------
/src/Routers/IAPValidationRouter.js:
--------------------------------------------------------------------------------
1 | import PromiseRouter from '../PromiseRouter';
2 | var request = require("request");
3 | var rest = require("../rest");
4 | var Auth = require("../Auth");
5 |
6 | // TODO move validation logic in IAPValidationController
7 | const IAP_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt";
8 | const IAP_PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt";
9 |
10 | const APP_STORE_ERRORS = {
11 | 21000: "The App Store could not read the JSON object you provided.",
12 | 21002: "The data in the receipt-data property was malformed or missing.",
13 | 21003: "The receipt could not be authenticated.",
14 | 21004: "The shared secret you provided does not match the shared secret on file for your account.",
15 | 21005: "The receipt server is not currently available.",
16 | 21006: "This receipt is valid but the subscription has expired.",
17 | 21007: "This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.",
18 | 21008: "This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead."
19 | }
20 |
21 | function appStoreError(status) {
22 | status = parseInt(status);
23 | var errorString = APP_STORE_ERRORS[status] || "unknown error.";
24 | return { status: status, error: errorString }
25 | }
26 |
27 | function validateWithAppStore(url, receipt) {
28 | return new Promise(function(fulfill, reject) {
29 | request.post({
30 | url: url,
31 | body: { "receipt-data": receipt },
32 | json: true,
33 | }, function(err, res, body) {
34 | var status = body.status;
35 | if (status == 0) {
36 | // No need to pass anything, status is OK
37 | return fulfill();
38 | }
39 | // receipt is from test and should go to test
40 | if (status == 21007) {
41 | return validateWithAppStore(IAP_SANDBOX_URL);
42 | }
43 | return reject(body);
44 | });
45 | });
46 | }
47 |
48 | function getFileForProductIdentifier(productIdentifier, req) {
49 | return rest.find(req.config, req.auth, '_Product', { productIdentifier: productIdentifier }).then(function(result){
50 | const products = result.results;
51 | if (!products || products.length != 1) {
52 | // Error not found or too many
53 | throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')
54 | }
55 |
56 | var download = products[0].download;
57 | return Promise.resolve({response: download});
58 | });
59 | }
60 |
61 |
62 |
63 | export class IAPValidationRouter extends PromiseRouter {
64 |
65 | handleRequest(req) {
66 | let receipt = req.body.receipt;
67 | const productIdentifier = req.body.productIdentifier;
68 |
69 | if (!receipt || ! productIdentifier) {
70 | // TODO: Error, malformed request
71 | throw new Parse.Error(Parse.Error.INVALID_JSON, "missing receipt or productIdentifier");
72 | }
73 |
74 | // Transform the object if there
75 | // otherwise assume it's in Base64 already
76 | if (typeof receipt == "object") {
77 | if (receipt["__type"] == "Bytes") {
78 | receipt = receipt.base64;
79 | }
80 | }
81 |
82 | if (process.env.NODE_ENV == "test" && req.body.bypassAppStoreValidation) {
83 | return getFileForProductIdentifier(productIdentifier, req);
84 | }
85 |
86 | return validateWithAppStore(IAP_PRODUCTION_URL, receipt).then( () => {
87 | return getFileForProductIdentifier(productIdentifier, req);
88 | }, (error) => {
89 | return Promise.resolve({response: appStoreError(error.status) });
90 | });
91 | }
92 |
93 | mountRoutes() {
94 | this.route("POST","/validate_purchase", this.handleRequest);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/spec/PushController.spec.js:
--------------------------------------------------------------------------------
1 | var PushController = require('../src/Controllers/PushController').PushController;
2 |
3 | describe('PushController', () => {
4 | it('can check valid master key of request', (done) => {
5 | // Make mock request
6 | var auth = {
7 | isMaster: true
8 | }
9 |
10 | expect(() => {
11 | PushController.validateMasterKey(auth);
12 | }).not.toThrow();
13 | done();
14 | });
15 |
16 | it('can check invalid master key of request', (done) => {
17 | // Make mock request
18 | var auth = {
19 | isMaster: false
20 | }
21 |
22 | expect(() => {
23 | PushController.validateMasterKey(auth);
24 | }).toThrow();
25 | done();
26 | });
27 |
28 |
29 | it('can validate device type when no device type is set', (done) => {
30 | // Make query condition
31 | var where = {
32 | };
33 | var validPushTypes = ['ios', 'android'];
34 |
35 | expect(function(){
36 | PushController.validatePushType(where, validPushTypes);
37 | }).not.toThrow();
38 | done();
39 | });
40 |
41 | it('can validate device type when single valid device type is set', (done) => {
42 | // Make query condition
43 | var where = {
44 | 'deviceType': 'ios'
45 | };
46 | var validPushTypes = ['ios', 'android'];
47 |
48 | expect(function(){
49 | PushController.validatePushType(where, validPushTypes);
50 | }).not.toThrow();
51 | done();
52 | });
53 |
54 | it('can validate device type when multiple valid device types are set', (done) => {
55 | // Make query condition
56 | var where = {
57 | 'deviceType': {
58 | '$in': ['android', 'ios']
59 | }
60 | };
61 | var validPushTypes = ['ios', 'android'];
62 |
63 | expect(function(){
64 | PushController.validatePushType(where, validPushTypes);
65 | }).not.toThrow();
66 | done();
67 | });
68 |
69 | it('can throw on validateDeviceType when single invalid device type is set', (done) => {
70 | // Make query condition
71 | var where = {
72 | 'deviceType': 'osx'
73 | };
74 | var validPushTypes = ['ios', 'android'];
75 |
76 | expect(function(){
77 | PushController.validatePushType(where, validPushTypes);
78 | }).toThrow();
79 | done();
80 | });
81 |
82 | it('can throw on validateDeviceType when single invalid device type is set', (done) => {
83 | // Make query condition
84 | var where = {
85 | 'deviceType': 'osx'
86 | };
87 | var validPushTypes = ['ios', 'android'];
88 |
89 | expect(function(){
90 | PushController.validatePushType(where, validPushTypes);
91 | }).toThrow();
92 | done();
93 | });
94 |
95 | it('can get expiration time in string format', (done) => {
96 | // Make mock request
97 | var timeStr = '2015-03-19T22:05:08Z';
98 | var body = {
99 | 'expiration_time': timeStr
100 | }
101 |
102 | var time = PushController.getExpirationTime(body);
103 | expect(time).toEqual(new Date(timeStr).valueOf());
104 | done();
105 | });
106 |
107 | it('can get expiration time in number format', (done) => {
108 | // Make mock request
109 | var timeNumber = 1426802708;
110 | var body = {
111 | 'expiration_time': timeNumber
112 | }
113 |
114 | var time = PushController.getExpirationTime(body);
115 | expect(time).toEqual(timeNumber * 1000);
116 | done();
117 | });
118 |
119 | it('can throw on getExpirationTime in invalid format', (done) => {
120 | // Make mock request
121 | var body = {
122 | 'expiration_time': 'abcd'
123 | }
124 |
125 | expect(function(){
126 | PushController.getExpirationTime(body);
127 | }).toThrow();
128 | done();
129 | });
130 |
131 | });
132 |
--------------------------------------------------------------------------------
/src/Controllers/PushController.js:
--------------------------------------------------------------------------------
1 | import { Parse } from 'parse/node';
2 | import PromiseRouter from '../PromiseRouter';
3 | import rest from '../rest';
4 | import AdaptableController from './AdaptableController';
5 | import { PushAdapter } from '../Adapters/Push/PushAdapter';
6 |
7 | export class PushController extends AdaptableController {
8 |
9 | /**
10 | * Check whether the deviceType parameter in qury condition is valid or not.
11 | * @param {Object} where A query condition
12 | * @param {Array} validPushTypes An array of valid push types(string)
13 | */
14 | static validatePushType(where = {}, validPushTypes = []) {
15 | var deviceTypeField = where.deviceType || {};
16 | var deviceTypes = [];
17 | if (typeof deviceTypeField === 'string') {
18 | deviceTypes.push(deviceTypeField);
19 | } else if (typeof deviceTypeField['$in'] === 'array') {
20 | deviceTypes.concat(deviceTypeField['$in']);
21 | }
22 | for (var i = 0; i < deviceTypes.length; i++) {
23 | var deviceType = deviceTypes[i];
24 | if (validPushTypes.indexOf(deviceType) < 0) {
25 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
26 | deviceType + ' is not supported push type.');
27 | }
28 | }
29 | }
30 |
31 | /**
32 | * Check whether the api call has master key or not.
33 | * @param {Object} request A request object
34 | */
35 | static validateMasterKey(auth = {}) {
36 | if (!auth.isMaster) {
37 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
38 | 'Master key is invalid, you should only use master key to send push');
39 | }
40 | }
41 |
42 | sendPush(body = {}, where = {}, config, auth) {
43 | var pushAdapter = this.adapter;
44 | if (!pushAdapter) {
45 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
46 | 'Push adapter is not available');
47 | }
48 | PushController.validateMasterKey(auth);
49 | PushController.validatePushType(where, pushAdapter.getValidPushTypes());
50 | // Replace the expiration_time with a valid Unix epoch milliseconds time
51 | body['expiration_time'] = PushController.getExpirationTime(body);
52 | // TODO: If the req can pass the checking, we return immediately instead of waiting
53 | // pushes to be sent. We probably change this behaviour in the future.
54 | rest.find(config, auth, '_Installation', where).then(function(response) {
55 | return pushAdapter.send(body, response.results);
56 | });
57 | }
58 |
59 | /**
60 | * Get expiration time from the request body.
61 | * @param {Object} request A request object
62 | * @returns {Number|undefined} The expiration time if it exists in the request
63 | */
64 | static getExpirationTime(body = {}) {
65 | var hasExpirationTime = !!body['expiration_time'];
66 | if (!hasExpirationTime) {
67 | return;
68 | }
69 | var expirationTimeParam = body['expiration_time'];
70 | var expirationTime;
71 | if (typeof expirationTimeParam === 'number') {
72 | expirationTime = new Date(expirationTimeParam * 1000);
73 | } else if (typeof expirationTimeParam === 'string') {
74 | expirationTime = new Date(expirationTimeParam);
75 | } else {
76 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
77 | body['expiration_time'] + ' is not valid time.');
78 | }
79 | // Check expirationTime is valid or not, if it is not valid, expirationTime is NaN
80 | if (!isFinite(expirationTime)) {
81 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
82 | body['expiration_time'] + ' is not valid time.');
83 | }
84 | return expirationTime.valueOf();
85 | }
86 |
87 | expectedAdapterType() {
88 | return PushAdapter;
89 | }
90 | };
91 |
92 | export default PushController;
93 |
--------------------------------------------------------------------------------
/src/Routers/ClassesRouter.js:
--------------------------------------------------------------------------------
1 |
2 | import PromiseRouter from '../PromiseRouter';
3 | import rest from '../rest';
4 |
5 | import url from 'url';
6 |
7 | export class ClassesRouter extends PromiseRouter {
8 |
9 | handleFind(req) {
10 | let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
11 | let options = {};
12 | let allowConstraints = ['skip', 'limit', 'order', 'count', 'keys',
13 | 'include', 'redirectClassNameForKey', 'where'];
14 |
15 | for (let key of Object.keys(body)) {
16 | if (allowConstraints.indexOf(key) === -1) {
17 | throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Improper encode of parameter');
18 | }
19 | }
20 |
21 | if (body.skip) {
22 | options.skip = Number(body.skip);
23 | }
24 | if (body.limit) {
25 | options.limit = Number(body.limit);
26 | } else {
27 | options.limit = Number(100);
28 | }
29 | if (body.order) {
30 | options.order = String(body.order);
31 | }
32 | if (body.count) {
33 | options.count = true;
34 | }
35 | if (typeof body.keys == 'string') {
36 | options.keys = body.keys;
37 | }
38 | if (body.include) {
39 | options.include = String(body.include);
40 | }
41 | if (body.redirectClassNameForKey) {
42 | options.redirectClassNameForKey = String(body.redirectClassNameForKey);
43 | }
44 | if (typeof body.where === 'string') {
45 | body.where = JSON.parse(body.where);
46 | }
47 | return rest.find(req.config, req.auth, req.params.className, body.where, options)
48 | .then((response) => {
49 | if (response && response.results) {
50 | for (let result of response.results) {
51 | if (result.sessionToken) {
52 | result.sessionToken = req.info.sessionToken || result.sessionToken;
53 | }
54 | }
55 | }
56 | return { response: response };
57 | });
58 | }
59 |
60 | // Returns a promise for a {response} object.
61 | handleGet(req) {
62 | return rest.find(req.config, req.auth, req.params.className, {objectId: req.params.objectId})
63 | .then((response) => {
64 | if (!response.results || response.results.length == 0) {
65 | throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
66 | }
67 |
68 | if (req.params.className === "_User") {
69 |
70 | delete response.results[0].sessionToken;
71 |
72 | const user = response.results[0];
73 |
74 | if (req.auth.user && user.objectId == req.auth.user.id) {
75 | // Force the session token
76 | response.results[0].sessionToken = req.info.sessionToken;
77 | }
78 | }
79 | return { response: response.results[0] };
80 | });
81 | }
82 |
83 | handleCreate(req) {
84 | return rest.create(req.config, req.auth, req.params.className, req.body);
85 | }
86 |
87 | handleUpdate(req) {
88 | return rest.update(req.config, req.auth, req.params.className, req.params.objectId, req.body)
89 | .then((response) => {
90 | return {response: response};
91 | });
92 | }
93 |
94 | handleDelete(req) {
95 | return rest.del(req.config, req.auth, req.params.className, req.params.objectId)
96 | .then(() => {
97 | return {response: {}};
98 | });
99 | }
100 |
101 | static JSONFromQuery(query) {
102 | let json = {};
103 | for (let [key, value] of Object.entries(query)) {
104 | try {
105 | json[key] = JSON.parse(value);
106 | } catch (e) {
107 | json[key] = value;
108 | }
109 | }
110 | return json
111 | }
112 |
113 | mountRoutes() {
114 | this.route('GET', '/classes/:className', (req) => { return this.handleFind(req); });
115 | this.route('GET', '/classes/:className/:objectId', (req) => { return this.handleGet(req); });
116 | this.route('POST', '/classes/:className', (req) => { return this.handleCreate(req); });
117 | this.route('PUT', '/classes/:className/:objectId', (req) => { return this.handleUpdate(req); });
118 | this.route('DELETE', '/classes/:className/:objectId', (req) => { return this.handleDelete(req); });
119 | }
120 | }
121 |
122 | export default ClassesRouter;
123 |
--------------------------------------------------------------------------------
/src/GCM.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const Parse = require('parse/node').Parse;
4 | const gcm = require('node-gcm');
5 | const cryptoUtils = require('./cryptoUtils');
6 |
7 | const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
8 | const GCMRegistrationTokensMax = 1000;
9 |
10 | function GCM(args) {
11 | if (typeof args !== 'object' || !args.apiKey) {
12 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
13 | 'GCM Configuration is invalid');
14 | }
15 | this.sender = new gcm.Sender(args.apiKey);
16 | }
17 |
18 | /**
19 | * Send gcm request.
20 | * @param {Object} data The data we need to send, the format is the same with api request body
21 | * @param {Array} devices A array of devices
22 | * @returns {Object} A promise which is resolved after we get results from gcm
23 | */
24 | GCM.prototype.send = function(data, devices) {
25 | let pushId = cryptoUtils.newObjectId();
26 | let timeStamp = Date.now();
27 | let expirationTime;
28 | // We handle the expiration_time convertion in push.js, so expiration_time is a valid date
29 | // in Unix epoch time in milliseconds here
30 | if (data['expiration_time']) {
31 | expirationTime = data['expiration_time'];
32 | }
33 | // Generate gcm payload
34 | let gcmPayload = generateGCMPayload(data.data, pushId, timeStamp, expirationTime);
35 | // Make and send gcm request
36 | let message = new gcm.Message(gcmPayload);
37 |
38 | let sendPromises = [];
39 | // For android, we can only have 1000 recepients per send, so we need to slice devices to
40 | // chunk if necessary
41 | let chunkDevices = sliceDevices(devices, GCMRegistrationTokensMax);
42 | for (let chunkDevice of chunkDevices) {
43 | let sendPromise = new Parse.Promise();
44 | let registrationTokens = []
45 | for (let device of chunkDevice) {
46 | registrationTokens.push(device.deviceToken);
47 | }
48 | this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => {
49 | // TODO: Use the response from gcm to generate and save push report
50 | // TODO: If gcm returns some deviceTokens are invalid, set tombstone for the installation
51 | console.log('GCM request and response %j', {
52 | request: message,
53 | response: response
54 | });
55 | sendPromise.resolve();
56 | });
57 | sendPromises.push(sendPromise);
58 | }
59 |
60 | return Parse.Promise.when(sendPromises);
61 | }
62 |
63 | /**
64 | * Generate the gcm payload from the data we get from api request.
65 | * @param {Object} coreData The data field under api request body
66 | * @param {String} pushId A random string
67 | * @param {Number} timeStamp A number whose format is the Unix Epoch
68 | * @param {Number|undefined} expirationTime A number whose format is the Unix Epoch or undefined
69 | * @returns {Object} A promise which is resolved after we get results from gcm
70 | */
71 | function generateGCMPayload(coreData, pushId, timeStamp, expirationTime) {
72 | let payloadData = {
73 | 'time': new Date(timeStamp).toISOString(),
74 | 'push_id': pushId,
75 | 'data': JSON.stringify(coreData)
76 | }
77 | let payload = {
78 | priority: 'normal',
79 | data: payloadData
80 | };
81 | if (expirationTime) {
82 | // The timeStamp and expiration is in milliseconds but gcm requires second
83 | let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
84 | if (timeToLive < 0) {
85 | timeToLive = 0;
86 | }
87 | if (timeToLive >= GCMTimeToLiveMax) {
88 | timeToLive = GCMTimeToLiveMax;
89 | }
90 | payload.timeToLive = timeToLive;
91 | }
92 | return payload;
93 | }
94 |
95 | /**
96 | * Slice a list of devices to several list of devices with fixed chunk size.
97 | * @param {Array} devices An array of devices
98 | * @param {Number} chunkSize The size of the a chunk
99 | * @returns {Array} An array which contaisn several arries of devices with fixed chunk size
100 | */
101 | function sliceDevices(devices, chunkSize) {
102 | let chunkDevices = [];
103 | while (devices.length > 0) {
104 | chunkDevices.push(devices.splice(0, chunkSize));
105 | }
106 | return chunkDevices;
107 | }
108 |
109 | if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
110 | GCM.generateGCMPayload = generateGCMPayload;
111 | GCM.sliceDevices = sliceDevices;
112 | }
113 | module.exports = GCM;
114 |
--------------------------------------------------------------------------------
/spec/ParsePushAdapter.spec.js:
--------------------------------------------------------------------------------
1 | var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter');
2 | var APNS = require('../src/APNS');
3 | var GCM = require('../src/GCM');
4 |
5 | describe('ParsePushAdapter', () => {
6 | it('can be initialized', (done) => {
7 | // Make mock config
8 | var pushConfig = {
9 | android: {
10 | senderId: 'senderId',
11 | apiKey: 'apiKey'
12 | },
13 | ios: [
14 | {
15 | cert: 'prodCert.pem',
16 | key: 'prodKey.pem',
17 | production: true,
18 | bundleId: 'bundleId'
19 | },
20 | {
21 | cert: 'devCert.pem',
22 | key: 'devKey.pem',
23 | production: false,
24 | bundleId: 'bundleIdAgain'
25 | }
26 | ]
27 | };
28 |
29 | var parsePushAdapter = new ParsePushAdapter(pushConfig);
30 | // Check ios
31 | var iosSender = parsePushAdapter.senderMap['ios'];
32 | expect(iosSender instanceof APNS).toBe(true);
33 | // Check android
34 | var androidSender = parsePushAdapter.senderMap['android'];
35 | expect(androidSender instanceof GCM).toBe(true);
36 | done();
37 | });
38 |
39 | it('can throw on initializing with unsupported push type', (done) => {
40 | // Make mock config
41 | var pushConfig = {
42 | win: {
43 | senderId: 'senderId',
44 | apiKey: 'apiKey'
45 | }
46 | };
47 |
48 | expect(function() {
49 | new ParsePushAdapter(pushConfig);
50 | }).toThrow();
51 | done();
52 | });
53 |
54 | it('can get valid push types', (done) => {
55 | var parsePushAdapter = new ParsePushAdapter();
56 |
57 | expect(parsePushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
58 | done();
59 | });
60 |
61 | it('can classify installation', (done) => {
62 | // Mock installations
63 | var validPushTypes = ['ios', 'android'];
64 | var installations = [
65 | {
66 | deviceType: 'android',
67 | deviceToken: 'androidToken'
68 | },
69 | {
70 | deviceType: 'ios',
71 | deviceToken: 'iosToken'
72 | },
73 | {
74 | deviceType: 'win',
75 | deviceToken: 'winToken'
76 | },
77 | {
78 | deviceType: 'android',
79 | deviceToken: undefined
80 | }
81 | ];
82 |
83 | var deviceMap = ParsePushAdapter.classifyInstallations(installations, validPushTypes);
84 | expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
85 | expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
86 | expect(deviceMap['win']).toBe(undefined);
87 | done();
88 | });
89 |
90 |
91 | it('can send push notifications', (done) => {
92 | var parsePushAdapter = new ParsePushAdapter();
93 | // Mock android ios senders
94 | var androidSender = {
95 | send: jasmine.createSpy('send')
96 | };
97 | var iosSender = {
98 | send: jasmine.createSpy('send')
99 | };
100 | var senderMap = {
101 | ios: iosSender,
102 | android: androidSender
103 | };
104 | parsePushAdapter.senderMap = senderMap;
105 | // Mock installations
106 | var installations = [
107 | {
108 | deviceType: 'android',
109 | deviceToken: 'androidToken'
110 | },
111 | {
112 | deviceType: 'ios',
113 | deviceToken: 'iosToken'
114 | },
115 | {
116 | deviceType: 'win',
117 | deviceToken: 'winToken'
118 | },
119 | {
120 | deviceType: 'android',
121 | deviceToken: undefined
122 | }
123 | ];
124 | var data = {};
125 |
126 | parsePushAdapter.send(data, installations);
127 | // Check android sender
128 | expect(androidSender.send).toHaveBeenCalled();
129 | var args = androidSender.send.calls.first().args;
130 | expect(args[0]).toEqual(data);
131 | expect(args[1]).toEqual([
132 | makeDevice('androidToken')
133 | ]);
134 | // Check ios sender
135 | expect(iosSender.send).toHaveBeenCalled();
136 | args = iosSender.send.calls.first().args;
137 | expect(args[0]).toEqual(data);
138 | expect(args[1]).toEqual([
139 | makeDevice('iosToken')
140 | ]);
141 | done();
142 | });
143 |
144 | function makeDevice(deviceToken, appIdentifier) {
145 | return {
146 | deviceToken: deviceToken,
147 | appIdentifier: appIdentifier
148 | };
149 | }
150 | });
151 |
--------------------------------------------------------------------------------
/spec/RestQuery.spec.js:
--------------------------------------------------------------------------------
1 | // These tests check the "find" functionality of the REST API.
2 | var auth = require('../src/Auth');
3 | var cache = require('../src/cache');
4 | var Config = require('../src/Config');
5 | var rest = require('../src/rest');
6 |
7 | var querystring = require('querystring');
8 | var request = require('request');
9 |
10 | var config = new Config('test');
11 | var nobody = auth.nobody(config);
12 |
13 | describe('rest query', () => {
14 | it('basic query', (done) => {
15 | rest.create(config, nobody, 'TestObject', {}).then(() => {
16 | return rest.find(config, nobody, 'TestObject', {});
17 | }).then((response) => {
18 | expect(response.results.length).toEqual(1);
19 | done();
20 | });
21 | });
22 |
23 | it('query with limit', (done) => {
24 | rest.create(config, nobody, 'TestObject', {foo: 'baz'}
25 | ).then(() => {
26 | return rest.create(config, nobody,
27 | 'TestObject', {foo: 'qux'});
28 | }).then(() => {
29 | return rest.find(config, nobody,
30 | 'TestObject', {}, {limit: 1});
31 | }).then((response) => {
32 | expect(response.results.length).toEqual(1);
33 | expect(response.results[0].foo).toBeTruthy();
34 | done();
35 | });
36 | });
37 |
38 | // Created to test a scenario in AnyPic
39 | it('query with include', (done) => {
40 | var photo = {
41 | foo: 'bar'
42 | };
43 | var user = {
44 | username: 'aUsername',
45 | password: 'aPassword'
46 | };
47 | var activity = {
48 | type: 'comment',
49 | photo: {
50 | __type: 'Pointer',
51 | className: 'TestPhoto',
52 | objectId: ''
53 | },
54 | fromUser: {
55 | __type: 'Pointer',
56 | className: '_User',
57 | objectId: ''
58 | }
59 | };
60 | var queryWhere = {
61 | photo: {
62 | __type: 'Pointer',
63 | className: 'TestPhoto',
64 | objectId: ''
65 | },
66 | type: 'comment'
67 | };
68 | var queryOptions = {
69 | include: 'fromUser',
70 | order: 'createdAt',
71 | limit: 30
72 | };
73 | rest.create(config, nobody, 'TestPhoto', photo
74 | ).then((p) => {
75 | photo = p;
76 | return rest.create(config, nobody, '_User', user);
77 | }).then((u) => {
78 | user = u.response;
79 | activity.photo.objectId = photo.objectId;
80 | activity.fromUser.objectId = user.objectId;
81 | return rest.create(config, nobody,
82 | 'TestActivity', activity);
83 | }).then(() => {
84 | queryWhere.photo.objectId = photo.objectId;
85 | return rest.find(config, nobody,
86 | 'TestActivity', queryWhere, queryOptions);
87 | }).then((response) => {
88 | var results = response.results;
89 | expect(results.length).toEqual(1);
90 | expect(typeof results[0].objectId).toEqual('string');
91 | expect(typeof results[0].photo).toEqual('object');
92 | expect(typeof results[0].fromUser).toEqual('object');
93 | expect(typeof results[0].fromUser.username).toEqual('string');
94 | done();
95 | }).catch((error) => { console.log(error); });
96 | });
97 |
98 | it('query with wrongly encoded parameter', (done) => {
99 | rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'}
100 | ).then(() => {
101 | return rest.create(config, nobody,
102 | 'TestParameterEncode', {foo: 'baz'});
103 | }).then(() => {
104 | var headers = {
105 | 'X-Parse-Application-Id': 'test',
106 | 'X-Parse-REST-API-Key': 'rest'
107 | };
108 | request.get({
109 | headers: headers,
110 | url: 'http://localhost:8378/1/classes/TestParameterEncode?'
111 | + querystring.stringify({
112 | where: '{"foo":{"$ne": "baz"}}',
113 | limit: 1
114 | }).replace('=', '%3D'),
115 | }, (error, response, body) => {
116 | expect(error).toBe(null);
117 | var b = JSON.parse(body);
118 | expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
119 | expect(b.error).toEqual('Improper encode of parameter');
120 | done();
121 | });
122 | }).then(() => {
123 | var headers = {
124 | 'X-Parse-Application-Id': 'test',
125 | 'X-Parse-REST-API-Key': 'rest'
126 | };
127 | request.get({
128 | headers: headers,
129 | url: 'http://localhost:8378/1/classes/TestParameterEncode?'
130 | + querystring.stringify({
131 | limit: 1
132 | }).replace('=', '%3D'),
133 | }, (error, response, body) => {
134 | expect(error).toBe(null);
135 | var b = JSON.parse(body);
136 | expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
137 | expect(b.error).toEqual('Improper encode of parameter');
138 | done();
139 | });
140 | });
141 | });
142 |
143 | });
144 |
--------------------------------------------------------------------------------
/src/rest.js:
--------------------------------------------------------------------------------
1 | // This file contains helpers for running operations in REST format.
2 | // The goal is that handlers that explicitly handle an express route
3 | // should just be shallow wrappers around things in this file, but
4 | // these functions should not explicitly depend on the request
5 | // object.
6 | // This means that one of these handlers can support multiple
7 | // routes. That's useful for the routes that do really similar
8 | // things.
9 |
10 | var Parse = require('parse/node').Parse;
11 |
12 | var cache = require('./cache');
13 | var RestQuery = require('./RestQuery');
14 | var RestWrite = require('./RestWrite');
15 | var triggers = require('./triggers');
16 |
17 | // Returns a promise for an object with optional keys 'results' and 'count'.
18 | function find(config, auth, className, restWhere, restOptions) {
19 | enforceRoleSecurity('find', className, auth);
20 | var query = new RestQuery(config, auth, className,
21 | restWhere, restOptions);
22 | return query.execute();
23 | }
24 |
25 | // Returns a promise that doesn't resolve to any useful value.
26 | function del(config, auth, className, objectId) {
27 | if (typeof objectId !== 'string') {
28 | throw new Parse.Error(Parse.Error.INVALID_JSON,
29 | 'bad objectId');
30 | }
31 |
32 | if (className === '_User' && !auth.couldUpdateUserId(objectId)) {
33 | throw new Parse.Error(Parse.Error.SESSION_MISSING,
34 | 'insufficient auth to delete user');
35 | }
36 |
37 | enforceRoleSecurity('delete', className, auth);
38 |
39 | var inflatedObject;
40 |
41 | return Promise.resolve().then(() => {
42 | if (triggers.getTrigger(className, 'beforeDelete') ||
43 | triggers.getTrigger(className, 'afterDelete') ||
44 | className == '_Session') {
45 | return find(config, auth, className, {objectId: objectId})
46 | .then((response) => {
47 | if (response && response.results && response.results.length) {
48 | response.results[0].className = className;
49 | cache.clearUser(response.results[0].sessionToken);
50 | inflatedObject = Parse.Object.fromJSON(response.results[0]);
51 | return triggers.maybeRunTrigger('beforeDelete',
52 | auth, inflatedObject);
53 | }
54 | throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
55 | 'Object not found for delete.');
56 | });
57 | }
58 | return Promise.resolve({});
59 | }).then(() => {
60 | if (!auth.isMaster) {
61 | return auth.getUserRoles();
62 | }else{
63 | return Promise.resolve();
64 | }
65 | }).then(() => {
66 | var options = {};
67 | if (!auth.isMaster) {
68 | options.acl = ['*'];
69 | if (auth.user) {
70 | options.acl.push(auth.user.id);
71 | options.acl = options.acl.concat(auth.userRoles);
72 | }
73 | }
74 |
75 | return config.database.destroy(className, {
76 | objectId: objectId
77 | }, options);
78 | }).then(() => {
79 | triggers.maybeRunTrigger('afterDelete', auth, inflatedObject);
80 | return Promise.resolve();
81 | });
82 | }
83 |
84 | // Returns a promise for a {response, status, location} object.
85 | function create(config, auth, className, restObject) {
86 | enforceRoleSecurity('create', className, auth);
87 |
88 | var write = new RestWrite(config, auth, className, null, restObject);
89 | return write.execute();
90 | }
91 |
92 | // Returns a promise that contains the fields of the update that the
93 | // REST API is supposed to return.
94 | // Usually, this is just updatedAt.
95 | function update(config, auth, className, objectId, restObject) {
96 | enforceRoleSecurity('update', className, auth);
97 |
98 | return Promise.resolve().then(() => {
99 | if (triggers.getTrigger(className, 'beforeSave') ||
100 | triggers.getTrigger(className, 'afterSave')) {
101 | return find(config, auth, className, {objectId: objectId});
102 | }
103 | return Promise.resolve({});
104 | }).then((response) => {
105 | var originalRestObject;
106 | if (response && response.results && response.results.length) {
107 | originalRestObject = response.results[0];
108 | }
109 |
110 | var write = new RestWrite(config, auth, className,
111 | {objectId: objectId}, restObject, originalRestObject);
112 | return write.execute();
113 | });
114 | }
115 |
116 | // Disallowing access to the _Role collection except by master key
117 | function enforceRoleSecurity(method, className, auth) {
118 | if (className === '_Role' && !auth.isMaster) {
119 | throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
120 | 'Clients aren\'t allowed to perform the ' +
121 | method + ' operation on the role collection.');
122 | }
123 | if (method === 'delete' && className === '_Installation' && !auth.isMaster) {
124 | throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
125 | 'Clients aren\'t allowed to perform the ' +
126 | 'delete operation on the installation collection.');
127 |
128 | }
129 | }
130 |
131 | module.exports = {
132 | create: create,
133 | del: del,
134 | find: find,
135 | update: update
136 | };
137 |
--------------------------------------------------------------------------------
/src/PromiseRouter.js:
--------------------------------------------------------------------------------
1 | // A router that is based on promises rather than req/res/next.
2 | // This is intended to replace the use of express.Router to handle
3 | // subsections of the API surface.
4 | // This will make it easier to have methods like 'batch' that
5 | // themselves use our routing information, without disturbing express
6 | // components that external developers may be modifying.
7 |
8 | export default class PromiseRouter {
9 | // Each entry should be an object with:
10 | // path: the path to route, in express format
11 | // method: the HTTP method that this route handles.
12 | // Must be one of: POST, GET, PUT, DELETE
13 | // handler: a function that takes request, and returns a promise.
14 | // Successful handlers should resolve to an object with fields:
15 | // status: optional. the http status code. defaults to 200
16 | // response: a json object with the content of the response
17 | // location: optional. a location header
18 | constructor() {
19 | this.routes = [];
20 | this.mountRoutes();
21 | }
22 |
23 | // Leave the opportunity to
24 | // subclasses to mount their routes by overriding
25 | mountRoutes() {}
26 |
27 | // Merge the routes into this one
28 | merge(router) {
29 | for (var route of router.routes) {
30 | this.routes.push(route);
31 | }
32 | };
33 |
34 | route(method, path, handler) {
35 | switch(method) {
36 | case 'POST':
37 | case 'GET':
38 | case 'PUT':
39 | case 'DELETE':
40 | break;
41 | default:
42 | throw 'cannot route method: ' + method;
43 | }
44 |
45 | this.routes.push({
46 | path: path,
47 | method: method,
48 | handler: handler
49 | });
50 | };
51 |
52 | // Returns an object with:
53 | // handler: the handler that should deal with this request
54 | // params: any :-params that got parsed from the path
55 | // Returns undefined if there is no match.
56 | match(method, path) {
57 | for (var route of this.routes) {
58 | if (route.method != method) {
59 | continue;
60 | }
61 |
62 | // NOTE: we can only route the specific wildcards :className and
63 | // :objectId, and in that order.
64 | // This is pretty hacky but I don't want to rebuild the entire
65 | // express route matcher. Maybe there's a way to reuse its logic.
66 | var pattern = '^' + route.path + '$';
67 |
68 | pattern = pattern.replace(':className',
69 | '(_?[A-Za-z][A-Za-z_0-9]*)');
70 | pattern = pattern.replace(':objectId',
71 | '([A-Za-z0-9]+)');
72 | var re = new RegExp(pattern);
73 | var m = path.match(re);
74 | if (!m) {
75 | continue;
76 | }
77 | var params = {};
78 | if (m[1]) {
79 | params.className = m[1];
80 | }
81 | if (m[2]) {
82 | params.objectId = m[2];
83 | }
84 |
85 | return {params: params, handler: route.handler};
86 | }
87 | };
88 |
89 | // Mount the routes on this router onto an express app (or express router)
90 | mountOnto(expressApp) {
91 | for (var route of this.routes) {
92 | switch(route.method) {
93 | case 'POST':
94 | expressApp.post(route.path, makeExpressHandler(route.handler));
95 | break;
96 | case 'GET':
97 | expressApp.get(route.path, makeExpressHandler(route.handler));
98 | break;
99 | case 'PUT':
100 | expressApp.put(route.path, makeExpressHandler(route.handler));
101 | break;
102 | case 'DELETE':
103 | expressApp.delete(route.path, makeExpressHandler(route.handler));
104 | break;
105 | default:
106 | throw 'unexpected code branch';
107 | }
108 | }
109 | };
110 | }
111 |
112 | // Global flag. Set this to true to log every request and response.
113 | PromiseRouter.verbose = process.env.VERBOSE || false;
114 |
115 | // A helper function to make an express handler out of a a promise
116 | // handler.
117 | // Express handlers should never throw; if a promise handler throws we
118 | // just treat it like it resolved to an error.
119 | function makeExpressHandler(promiseHandler) {
120 | return function(req, res, next) {
121 | try {
122 | if (PromiseRouter.verbose) {
123 | console.log(req.method, req.originalUrl, req.headers,
124 | JSON.stringify(req.body, null, 2));
125 | }
126 | promiseHandler(req).then((result) => {
127 | if (!result.response) {
128 | console.log('BUG: the handler did not include a "response" field');
129 | throw 'control should not get here';
130 | }
131 | if (PromiseRouter.verbose) {
132 | console.log('response:', JSON.stringify(result.response, null, 2));
133 | }
134 | var status = result.status || 200;
135 | res.status(status);
136 | if (result.location) {
137 | res.set('Location', result.location);
138 | }
139 | res.json(result.response);
140 | }, (e) => {
141 | if (PromiseRouter.verbose) {
142 | console.log('error:', e);
143 | }
144 | next(e);
145 | });
146 | } catch (e) {
147 | if (PromiseRouter.verbose) {
148 | console.log('error:', e);
149 | }
150 | next(e);
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/spec/GCM.spec.js:
--------------------------------------------------------------------------------
1 | var GCM = require('../src/GCM');
2 |
3 | describe('GCM', () => {
4 | it('can initialize', (done) => {
5 | var args = {
6 | apiKey: 'apiKey'
7 | };
8 | var gcm = new GCM(args);
9 | expect(gcm.sender.key).toBe(args.apiKey);
10 | done();
11 | });
12 |
13 | it('can throw on initializing with invalid args', (done) => {
14 | var args = 123
15 | expect(function() {
16 | new GCM(args);
17 | }).toThrow();
18 | done();
19 | });
20 |
21 | it('can generate GCM Payload without expiration time', (done) => {
22 | //Mock request data
23 | var data = {
24 | 'alert': 'alert'
25 | };
26 | var pushId = 1;
27 | var timeStamp = 1454538822113;
28 | var timeStampISOStr = new Date(timeStamp).toISOString();
29 |
30 | var payload = GCM.generateGCMPayload(data, pushId, timeStamp);
31 |
32 | expect(payload.priority).toEqual('normal');
33 | expect(payload.timeToLive).toEqual(undefined);
34 | var dataFromPayload = payload.data;
35 | expect(dataFromPayload.time).toEqual(timeStampISOStr);
36 | expect(dataFromPayload['push_id']).toEqual(pushId);
37 | var dataFromUser = JSON.parse(dataFromPayload.data);
38 | expect(dataFromUser).toEqual(data);
39 | done();
40 | });
41 |
42 | it('can generate GCM Payload with valid expiration time', (done) => {
43 | //Mock request data
44 | var data = {
45 | 'alert': 'alert'
46 | };
47 | var pushId = 1;
48 | var timeStamp = 1454538822113;
49 | var timeStampISOStr = new Date(timeStamp).toISOString();
50 | var expirationTime = 1454538922113
51 |
52 | var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
53 |
54 | expect(payload.priority).toEqual('normal');
55 | expect(payload.timeToLive).toEqual(Math.floor((expirationTime - timeStamp) / 1000));
56 | var dataFromPayload = payload.data;
57 | expect(dataFromPayload.time).toEqual(timeStampISOStr);
58 | expect(dataFromPayload['push_id']).toEqual(pushId);
59 | var dataFromUser = JSON.parse(dataFromPayload.data);
60 | expect(dataFromUser).toEqual(data);
61 | done();
62 | });
63 |
64 | it('can generate GCM Payload with too early expiration time', (done) => {
65 | //Mock request data
66 | var data = {
67 | 'alert': 'alert'
68 | };
69 | var pushId = 1;
70 | var timeStamp = 1454538822113;
71 | var timeStampISOStr = new Date(timeStamp).toISOString();
72 | var expirationTime = 1454538822112;
73 |
74 | var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
75 |
76 | expect(payload.priority).toEqual('normal');
77 | expect(payload.timeToLive).toEqual(0);
78 | var dataFromPayload = payload.data;
79 | expect(dataFromPayload.time).toEqual(timeStampISOStr);
80 | expect(dataFromPayload['push_id']).toEqual(pushId);
81 | var dataFromUser = JSON.parse(dataFromPayload.data);
82 | expect(dataFromUser).toEqual(data);
83 | done();
84 | });
85 |
86 | it('can generate GCM Payload with too late expiration time', (done) => {
87 | //Mock request data
88 | var data = {
89 | 'alert': 'alert'
90 | };
91 | var pushId = 1;
92 | var timeStamp = 1454538822113;
93 | var timeStampISOStr = new Date(timeStamp).toISOString();
94 | var expirationTime = 2454538822113;
95 |
96 | var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
97 |
98 | expect(payload.priority).toEqual('normal');
99 | // Four week in second
100 | expect(payload.timeToLive).toEqual(4 * 7 * 24 * 60 * 60);
101 | var dataFromPayload = payload.data;
102 | expect(dataFromPayload.time).toEqual(timeStampISOStr);
103 | expect(dataFromPayload['push_id']).toEqual(pushId);
104 | var dataFromUser = JSON.parse(dataFromPayload.data);
105 | expect(dataFromUser).toEqual(data);
106 | done();
107 | });
108 |
109 | it('can send GCM request', (done) => {
110 | var gcm = new GCM({
111 | apiKey: 'apiKey'
112 | });
113 | // Mock gcm sender
114 | var sender = {
115 | send: jasmine.createSpy('send')
116 | };
117 | gcm.sender = sender;
118 | // Mock data
119 | var expirationTime = 2454538822113;
120 | var data = {
121 | 'expiration_time': expirationTime,
122 | 'data': {
123 | 'alert': 'alert'
124 | }
125 | }
126 | // Mock devices
127 | var devices = [
128 | {
129 | deviceToken: 'token'
130 | }
131 | ];
132 |
133 | gcm.send(data, devices);
134 | expect(sender.send).toHaveBeenCalled();
135 | var args = sender.send.calls.first().args;
136 | // It is too hard to verify message of gcm library, we just verify tokens and retry times
137 | expect(args[1].registrationTokens).toEqual(['token']);
138 | expect(args[2]).toEqual(5);
139 | done();
140 | });
141 |
142 | it('can slice devices', (done) => {
143 | // Mock devices
144 | var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)];
145 |
146 | var chunkDevices = GCM.sliceDevices(devices, 3);
147 | expect(chunkDevices).toEqual([
148 | [makeDevice(1), makeDevice(2), makeDevice(3)],
149 | [makeDevice(4)]
150 | ]);
151 | done();
152 | });
153 |
154 | function makeDevice(deviceToken) {
155 | return {
156 | deviceToken: deviceToken
157 | };
158 | }
159 | });
160 |
--------------------------------------------------------------------------------
/src/Auth.js:
--------------------------------------------------------------------------------
1 | var deepcopy = require('deepcopy');
2 | var Parse = require('parse/node').Parse;
3 | var RestQuery = require('./RestQuery');
4 |
5 | var cache = require('./cache');
6 |
7 | // An Auth object tells you who is requesting something and whether
8 | // the master key was used.
9 | // userObject is a Parse.User and can be null if there's no user.
10 | function Auth(config, isMaster, userObject) {
11 | this.config = config;
12 | this.isMaster = isMaster;
13 | this.user = userObject;
14 |
15 | // Assuming a users roles won't change during a single request, we'll
16 | // only load them once.
17 | this.userRoles = [];
18 | this.fetchedRoles = false;
19 | this.rolePromise = null;
20 | }
21 |
22 | // Whether this auth could possibly modify the given user id.
23 | // It still could be forbidden via ACLs even if this returns true.
24 | Auth.prototype.couldUpdateUserId = function(userId) {
25 | if (this.isMaster) {
26 | return true;
27 | }
28 | if (this.user && this.user.id === userId) {
29 | return true;
30 | }
31 | return false;
32 | };
33 |
34 | // A helper to get a master-level Auth object
35 | function master(config) {
36 | return new Auth(config, true, null);
37 | }
38 |
39 | // A helper to get a nobody-level Auth object
40 | function nobody(config) {
41 | return new Auth(config, false, null);
42 | }
43 |
44 | // Returns a promise that resolves to an Auth object
45 | var getAuthForSessionToken = function(config, sessionToken) {
46 | var cachedUser = cache.getUser(sessionToken);
47 | if (cachedUser) {
48 | return Promise.resolve(new Auth(config, false, cachedUser));
49 | }
50 | var restOptions = {
51 | limit: 1,
52 | include: 'user'
53 | };
54 | var restWhere = {
55 | _session_token: sessionToken
56 | };
57 | var query = new RestQuery(config, master(config), '_Session',
58 | restWhere, restOptions);
59 | return query.execute().then((response) => {
60 | var results = response.results;
61 | if (results.length !== 1 || !results[0]['user']) {
62 | return nobody(config);
63 | }
64 | var obj = results[0]['user'];
65 | delete obj.password;
66 | obj['className'] = '_User';
67 | obj['sessionToken'] = sessionToken;
68 | var userObject = Parse.Object.fromJSON(obj);
69 | cache.setUser(sessionToken, userObject);
70 | return new Auth(config, false, userObject);
71 | });
72 | };
73 |
74 | // Returns a promise that resolves to an array of role names
75 | Auth.prototype.getUserRoles = function() {
76 | if (this.isMaster || !this.user) {
77 | return Promise.resolve([]);
78 | }
79 | if (this.fetchedRoles) {
80 | return Promise.resolve(this.userRoles);
81 | }
82 | if (this.rolePromise) {
83 | return this.rolePromise;
84 | }
85 | this.rolePromise = this._loadRoles();
86 | return this.rolePromise;
87 | };
88 |
89 | // Iterates through the role tree and compiles a users roles
90 | Auth.prototype._loadRoles = function() {
91 | var restWhere = {
92 | 'users': {
93 | __type: 'Pointer',
94 | className: '_User',
95 | objectId: this.user.id
96 | }
97 | };
98 | // First get the role ids this user is directly a member of
99 | var query = new RestQuery(this.config, master(this.config), '_Role',
100 | restWhere, {});
101 | return query.execute().then((response) => {
102 | var results = response.results;
103 | if (!results.length) {
104 | this.userRoles = [];
105 | this.fetchedRoles = true;
106 | this.rolePromise = null;
107 | return Promise.resolve(this.userRoles);
108 | }
109 |
110 | var roleIDs = results.map(r => r.objectId);
111 | var promises = [Promise.resolve(roleIDs)];
112 | for (var role of roleIDs) {
113 | promises.push(this._getAllRoleNamesForId(role));
114 | }
115 | return Promise.all(promises).then((results) => {
116 | var allIDs = [];
117 | for (var x of results) {
118 | Array.prototype.push.apply(allIDs, x);
119 | }
120 | var restWhere = {
121 | objectId: {
122 | '$in': allIDs
123 | }
124 | };
125 | var query = new RestQuery(this.config, master(this.config),
126 | '_Role', restWhere, {});
127 | return query.execute();
128 | }).then((response) => {
129 | var results = response.results;
130 | this.userRoles = results.map((r) => {
131 | return 'role:' + r.name;
132 | });
133 | this.fetchedRoles = true;
134 | this.rolePromise = null;
135 | return Promise.resolve(this.userRoles);
136 | });
137 | });
138 | };
139 |
140 | // Given a role object id, get any other roles it is part of
141 | // TODO: Make recursive to support role nesting beyond 1 level deep
142 | Auth.prototype._getAllRoleNamesForId = function(roleID) {
143 | var rolePointer = {
144 | __type: 'Pointer',
145 | className: '_Role',
146 | objectId: roleID
147 | };
148 | var restWhere = {
149 | '$relatedTo': {
150 | key: 'roles',
151 | object: rolePointer
152 | }
153 | };
154 | var query = new RestQuery(this.config, master(this.config), '_Role',
155 | restWhere, {});
156 | return query.execute().then((response) => {
157 | var results = response.results;
158 | if (!results.length) {
159 | return Promise.resolve([]);
160 | }
161 | var roleIDs = results.map(r => r.objectId);
162 | return Promise.resolve(roleIDs);
163 | });
164 | };
165 |
166 | module.exports = {
167 | Auth: Auth,
168 | master: master,
169 | nobody: nobody,
170 | getAuthForSessionToken: getAuthForSessionToken
171 | };
172 |
--------------------------------------------------------------------------------
/src/Routers/UsersRouter.js:
--------------------------------------------------------------------------------
1 | // These methods handle the User-related routes.
2 |
3 | import deepcopy from 'deepcopy';
4 |
5 | import ClassesRouter from './ClassesRouter';
6 | import PromiseRouter from '../PromiseRouter';
7 | import rest from '../rest';
8 | import Auth from '../Auth';
9 | import passwordCrypto from '../password';
10 | import RestWrite from '../RestWrite';
11 | import { newToken } from '../cryptoUtils';
12 |
13 | export class UsersRouter extends ClassesRouter {
14 | handleFind(req) {
15 | req.params.className = '_User';
16 | return super.handleFind(req);
17 | }
18 |
19 | handleGet(req) {
20 | req.params.className = '_User';
21 | return super.handleGet(req);
22 | }
23 |
24 | handleCreate(req) {
25 | let data = deepcopy(req.body);
26 | req.body = data;
27 | req.params.className = '_User';
28 | return super.handleCreate(req);
29 | }
30 |
31 | handleUpdate(req) {
32 | req.params.className = '_User';
33 | return super.handleUpdate(req);
34 | }
35 |
36 | handleDelete(req) {
37 | req.params.className = '_User';
38 | return super.handleDelete(req);
39 | }
40 |
41 | handleMe(req) {
42 | if (!req.info || !req.info.sessionToken) {
43 | throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid session token');
44 | }
45 | return rest.find(req.config, Auth.master(req.config), '_Session',
46 | { _session_token: req.info.sessionToken },
47 | { include: 'user' })
48 | .then((response) => {
49 | if (!response.results ||
50 | response.results.length == 0 ||
51 | !response.results[0].user) {
52 | throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid session token');
53 | } else {
54 | let user = response.results[0].user;
55 | return { response: user };
56 | }
57 | });
58 | }
59 |
60 | handleLogIn(req) {
61 | // Use query parameters instead if provided in url
62 | if (!req.body.username && req.query.username) {
63 | req.body = req.query;
64 | }
65 |
66 | // TODO: use the right error codes / descriptions.
67 | if (!req.body.username) {
68 | throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'username is required.');
69 | }
70 | if (!req.body.password) {
71 | throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'password is required.');
72 | }
73 |
74 | let user;
75 | return req.database.find('_User', { username: req.body.username })
76 | .then((results) => {
77 | if (!results.length) {
78 | throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
79 | }
80 | user = results[0];
81 | return passwordCrypto.compare(req.body.password, user.password);
82 | }).then((correct) => {
83 | if (!correct) {
84 | throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
85 | }
86 |
87 | let token = 'r:' + newToken();
88 | user.sessionToken = token;
89 | delete user.password;
90 |
91 | req.config.filesController.expandFilesInObject(req.config, user);
92 |
93 | let expiresAt = new Date();
94 | expiresAt.setFullYear(expiresAt.getFullYear() + 1);
95 |
96 | let sessionData = {
97 | sessionToken: token,
98 | user: {
99 | __type: 'Pointer',
100 | className: '_User',
101 | objectId: user.objectId
102 | },
103 | createdWith: {
104 | 'action': 'login',
105 | 'authProvider': 'password'
106 | },
107 | restricted: false,
108 | expiresAt: Parse._encode(expiresAt)
109 | };
110 |
111 | if (req.info.installationId) {
112 | sessionData.installationId = req.info.installationId
113 | }
114 |
115 | let create = new RestWrite(req.config, Auth.master(req.config), '_Session', null, sessionData);
116 | return create.execute();
117 | }).then(() => {
118 | return { response: user };
119 | });
120 | }
121 |
122 | handleLogOut(req) {
123 | let success = {response: {}};
124 | if (req.info && req.info.sessionToken) {
125 | return rest.find(req.config, Auth.master(req.config), '_Session',
126 | { _session_token: req.info.sessionToken }
127 | ).then((records) => {
128 | if (records.results && records.results.length) {
129 | return rest.del(req.config, Auth.master(req.config), '_Session',
130 | records.results[0].objectId
131 | ).then(() => {
132 | return Promise.resolve(success);
133 | });
134 | }
135 | return Promise.resolve(success);
136 | });
137 | }
138 | return Promise.resolve(success);
139 | }
140 |
141 | mountRoutes() {
142 | this.route('GET', '/users', req => { return this.handleFind(req); });
143 | this.route('POST', '/users', req => { return this.handleCreate(req); });
144 | this.route('GET', '/users/me', req => { return this.handleMe(req); });
145 | this.route('GET', '/users/:objectId', req => { return this.handleGet(req); });
146 | this.route('PUT', '/users/:objectId', req => { return this.handleUpdate(req); });
147 | this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
148 | this.route('GET', '/login', req => { return this.handleLogIn(req); });
149 | this.route('POST', '/logout', req => { return this.handleLogOut(req); });
150 | this.route('POST', '/requestPasswordReset', () => {
151 | throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'This path is not implemented yet.');
152 | });
153 | }
154 | }
155 |
156 | export default UsersRouter;
157 |
--------------------------------------------------------------------------------
/spec/HTTPRequest.spec.js:
--------------------------------------------------------------------------------
1 | var httpRequest = require("../src/httpRequest"),
2 | bodyParser = require('body-parser'),
3 | express = require("express");
4 |
5 | var port = 13371;
6 | var httpRequestServer = "http://localhost:"+port;
7 |
8 | var app = express();
9 | app.use(bodyParser.json({ 'type': '*/*' }));
10 | app.get("/hello", function(req, res){
11 | res.json({response: "OK"});
12 | });
13 |
14 | app.get("/404", function(req, res){
15 | res.status(404);
16 | res.send("NO");
17 | });
18 |
19 | app.get("/301", function(req, res){
20 | res.status(301);
21 | res.location("/hello");
22 | res.send();
23 | });
24 |
25 | app.post('/echo', function(req, res){
26 | res.json(req.body);
27 | })
28 |
29 | app.listen(13371);
30 |
31 |
32 | describe("httpRequest", () => {
33 |
34 | it("should do /hello", (done) => {
35 | httpRequest({
36 | url: httpRequestServer+"/hello"
37 | }).then(function(httpResponse){
38 | expect(httpResponse.status).toBe(200);
39 | expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
40 | expect(httpResponse.text).toEqual('{"response":"OK"}');
41 | expect(httpResponse.data.response).toEqual("OK");
42 | done();
43 | }, function(){
44 | fail("should not fail");
45 | done();
46 | })
47 | });
48 |
49 | it("should do /hello with callback and promises", (done) => {
50 | var calls = 0;
51 | httpRequest({
52 | url: httpRequestServer+"/hello",
53 | success: function() { calls++; },
54 | error: function() { calls++; }
55 | }).then(function(httpResponse){
56 | expect(calls).toBe(1);
57 | expect(httpResponse.status).toBe(200);
58 | expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
59 | expect(httpResponse.text).toEqual('{"response":"OK"}');
60 | expect(httpResponse.data.response).toEqual("OK");
61 | done();
62 | }, function(){
63 | fail("should not fail");
64 | done();
65 | })
66 | });
67 |
68 | it("should do not follow redirects by default", (done) => {
69 |
70 | httpRequest({
71 | url: httpRequestServer+"/301"
72 | }).then(function(httpResponse){
73 | expect(httpResponse.status).toBe(301);
74 | done();
75 | }, function(){
76 | fail("should not fail");
77 | done();
78 | })
79 | });
80 |
81 | it("should follow redirects when set", (done) => {
82 |
83 | httpRequest({
84 | url: httpRequestServer+"/301",
85 | followRedirects: true
86 | }).then(function(httpResponse){
87 | expect(httpResponse.status).toBe(200);
88 | expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
89 | expect(httpResponse.text).toEqual('{"response":"OK"}');
90 | expect(httpResponse.data.response).toEqual("OK");
91 | done();
92 | }, function(){
93 | fail("should not fail");
94 | done();
95 | })
96 | });
97 |
98 | it("should fail on 404", (done) => {
99 | var calls = 0;
100 | httpRequest({
101 | url: httpRequestServer+"/404",
102 | success: function() {
103 | calls++;
104 | fail("should not succeed");
105 | done();
106 | },
107 | error: function(httpResponse) {
108 | calls++;
109 | expect(calls).toBe(1);
110 | expect(httpResponse.status).toBe(404);
111 | expect(httpResponse.buffer).toEqual(new Buffer('NO'));
112 | expect(httpResponse.text).toEqual('NO');
113 | expect(httpResponse.data).toBe(undefined);
114 | done();
115 | }
116 | });
117 | })
118 |
119 | it("should fail on 404", (done) => {
120 | httpRequest({
121 | url: httpRequestServer+"/404",
122 | }).then(function(httpResponse){
123 | fail("should not succeed");
124 | done();
125 | }, function(httpResponse){
126 | expect(httpResponse.status).toBe(404);
127 | expect(httpResponse.buffer).toEqual(new Buffer('NO'));
128 | expect(httpResponse.text).toEqual('NO');
129 | expect(httpResponse.data).toBe(undefined);
130 | done();
131 | })
132 | })
133 |
134 | it("should post on echo", (done) => {
135 | var calls = 0;
136 | httpRequest({
137 | method: "POST",
138 | url: httpRequestServer+"/echo",
139 | body: {
140 | foo: "bar"
141 | },
142 | headers: {
143 | 'Content-Type': 'application/json'
144 | },
145 | success: function() { calls++; },
146 | error: function() { calls++; }
147 | }).then(function(httpResponse){
148 | expect(calls).toBe(1);
149 | expect(httpResponse.status).toBe(200);
150 | expect(httpResponse.data).toEqual({foo: "bar"});
151 | done();
152 | }, function(httpResponse){
153 | fail("should not fail");
154 | done();
155 | })
156 | });
157 | it("should encode a JSON body", (done) => {
158 |
159 | var result = httpRequest.encodeBody({"foo": "bar"}, {'Content-Type': 'application/json'});
160 | expect(result).toEqual('{"foo":"bar"}');
161 | done();
162 |
163 | })
164 | it("should encode a www-form body", (done) => {
165 |
166 | var result = httpRequest.encodeBody({"foo": "bar", "bar": "baz"}, {'cOntent-tYpe': 'application/x-www-form-urlencoded'});
167 | expect(result).toEqual("foo=bar&bar=baz");
168 | done();
169 | });
170 | it("should not encode a wrong content type", (done) => {
171 |
172 | var result = httpRequest.encodeBody({"foo": "bar", "bar": "baz"}, {'cOntent-tYpe': 'mime/jpeg'});
173 | expect(result).toEqual({"foo": "bar", "bar": "baz"});
174 | done();
175 | });
176 | it("should not encode when missing content type", (done) => {
177 | var result = httpRequest.encodeBody({"foo": "bar", "bar": "baz"}, {'X-Custom-Header': 'my-header'});
178 | expect(result).toEqual({"foo": "bar", "bar": "baz"});
179 | done();
180 | })
181 | });
182 |
--------------------------------------------------------------------------------
/src/Adapters/Push/OneSignalPushAdapter.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | // ParsePushAdapter is the default implementation of
3 | // PushAdapter, it uses GCM for android push and APNS
4 | // for ios push.
5 |
6 | import { classifyInstallations } from './PushAdapterUtils';
7 |
8 | const Parse = require('parse/node').Parse;
9 | var deepcopy = require('deepcopy');
10 | import PushAdapter from './PushAdapter';
11 |
12 | export class OneSignalPushAdapter extends PushAdapter {
13 |
14 | constructor(pushConfig = {}) {
15 | super(pushConfig);
16 | this.https = require('https');
17 |
18 | this.validPushTypes = ['ios', 'android'];
19 | this.senderMap = {};
20 | this.OneSignalConfig = {};
21 | this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
22 | this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
23 |
24 | this.senderMap['ios'] = this.sendToAPNS.bind(this);
25 | this.senderMap['android'] = this.sendToGCM.bind(this);
26 | }
27 |
28 | send(data, installations) {
29 | console.log("Sending notification to "+installations.length+" devices.")
30 | let deviceMap = classifyInstallations(installations, this.validPushTypes);
31 |
32 | let sendPromises = [];
33 | for (let pushType in deviceMap) {
34 | let sender = this.senderMap[pushType];
35 | if (!sender) {
36 | console.log('Can not find sender for push type %s, %j', pushType, data);
37 | continue;
38 | }
39 | let devices = deviceMap[pushType];
40 |
41 | if(devices.length > 0) {
42 | sendPromises.push(sender(data, devices));
43 | }
44 | }
45 | return Parse.Promise.when(sendPromises);
46 | }
47 |
48 | static classifyInstallations(installations, validTypes) {
49 | return classifyInstallations(installations, validTypes)
50 | }
51 |
52 | getValidPushTypes() {
53 | return this.validPushTypes;
54 | }
55 |
56 | sendToAPNS(data,tokens) {
57 |
58 | data= deepcopy(data['data']);
59 |
60 | var post = {};
61 | if(data['badge']) {
62 | if(data['badge'] == "Increment") {
63 | post['ios_badgeType'] = 'Increase';
64 | post['ios_badgeCount'] = 1;
65 | } else {
66 | post['ios_badgeType'] = 'SetTo';
67 | post['ios_badgeCount'] = data['badge'];
68 | }
69 | delete data['badge'];
70 | }
71 | if(data['alert']) {
72 | post['contents'] = {en: data['alert']};
73 | delete data['alert'];
74 | }
75 | if(data['sound']) {
76 | post['ios_sound'] = data['sound'];
77 | delete data['sound'];
78 | }
79 | if(data['content-available'] == 1) {
80 | post['content_available'] = true;
81 | delete data['content-available'];
82 | }
83 | post['data'] = data;
84 |
85 | let promise = new Parse.Promise();
86 |
87 | var chunk = 2000 // OneSignal can process 2000 devices at a time
88 | var tokenlength=tokens.length;
89 | var offset = 0
90 | // handle onesignal response. Start next batch if there's not an error.
91 | let handleResponse = function(wasSuccessful) {
92 | if (!wasSuccessful) {
93 | return promise.reject("OneSignal Error");
94 | }
95 |
96 | if(offset >= tokenlength) {
97 | promise.resolve()
98 | } else {
99 | this.sendNext();
100 | }
101 | }.bind(this)
102 |
103 | this.sendNext = function() {
104 | post['include_ios_tokens'] = [];
105 | tokens.slice(offset,offset+chunk).forEach(function(i) {
106 | post['include_ios_tokens'].push(i['deviceToken'])
107 | })
108 | offset+=chunk;
109 | this.sendToOneSignal(post, handleResponse);
110 | }.bind(this)
111 |
112 | this.sendNext()
113 |
114 | return promise;
115 | }
116 |
117 | sendToGCM(data,tokens) {
118 | data= deepcopy(data['data']);
119 |
120 | var post = {};
121 |
122 | if(data['alert']) {
123 | post['contents'] = {en: data['alert']};
124 | delete data['alert'];
125 | }
126 | if(data['title']) {
127 | post['title'] = {en: data['title']};
128 | delete data['title'];
129 | }
130 | if(data['uri']) {
131 | post['url'] = data['uri'];
132 | }
133 |
134 | post['data'] = data;
135 |
136 | let promise = new Parse.Promise();
137 |
138 | var chunk = 2000 // OneSignal can process 2000 devices at a time
139 | var tokenlength=tokens.length;
140 | var offset = 0
141 | // handle onesignal response. Start next batch if there's not an error.
142 | let handleResponse = function(wasSuccessful) {
143 | if (!wasSuccessful) {
144 | return promise.reject("OneSIgnal Error");
145 | }
146 |
147 | if(offset >= tokenlength) {
148 | promise.resolve()
149 | } else {
150 | this.sendNext();
151 | }
152 | }.bind(this);
153 |
154 | this.sendNext = function() {
155 | post['include_android_reg_ids'] = [];
156 | tokens.slice(offset,offset+chunk).forEach(function(i) {
157 | post['include_android_reg_ids'].push(i['deviceToken'])
158 | })
159 | offset+=chunk;
160 | this.sendToOneSignal(post, handleResponse);
161 | }.bind(this)
162 |
163 |
164 | this.sendNext();
165 | return promise;
166 | }
167 |
168 | sendToOneSignal(data, cb) {
169 | let headers = {
170 | "Content-Type": "application/json",
171 | "Authorization": "Basic "+this.OneSignalConfig['apiKey']
172 | };
173 | let options = {
174 | host: "onesignal.com",
175 | port: 443,
176 | path: "/api/v1/notifications",
177 | method: "POST",
178 | headers: headers
179 | };
180 | data['app_id'] = this.OneSignalConfig['appId'];
181 |
182 | let request = this.https.request(options, function(res) {
183 | if(res.statusCode < 299) {
184 | cb(true);
185 | } else {
186 | console.log('OneSignal Error');
187 | res.on('data', function(chunk) {
188 | console.log(chunk.toString())
189 | });
190 | cb(false)
191 | }
192 | });
193 | request.on('error', function(e) {
194 | console.log("Error connecting to OneSignal")
195 | console.log(e);
196 | cb(false);
197 | });
198 | request.write(JSON.stringify(data))
199 | request.end();
200 | }
201 | }
202 |
203 |
204 | export default OneSignalPushAdapter;
205 | module.exports = OneSignalPushAdapter;
206 |
--------------------------------------------------------------------------------
/spec/PurchaseValidation.spec.js:
--------------------------------------------------------------------------------
1 | var request = require("request");
2 |
3 |
4 |
5 | function createProduct() {
6 | const file = new Parse.File("name", {
7 | base64: new Buffer("download_file", "utf-8").toString("base64")
8 | }, "text");
9 | return file.save().then(function(){
10 | var product = new Parse.Object("_Product");
11 | product.set({
12 | download: file,
13 | icon: file,
14 | title: "a product",
15 | subtitle: "a product",
16 | order: 1,
17 | productIdentifier: "a-product"
18 | })
19 | return product.save();
20 | })
21 |
22 | }
23 |
24 |
25 | describe("test validate_receipt endpoint", () => {
26 |
27 | beforeEach( done => {
28 | createProduct().then(done).fail(function(err){
29 | console.error(err);
30 | done();
31 | })
32 | })
33 |
34 | it("should bypass appstore validation", (done) => {
35 |
36 | request.post({
37 | headers: {
38 | 'X-Parse-Application-Id': 'test',
39 | 'X-Parse-REST-API-Key': 'rest'},
40 | url: 'http://localhost:8378/1/validate_purchase',
41 | json: true,
42 | body: {
43 | productIdentifier: "a-product",
44 | receipt: {
45 | __type: "Bytes",
46 | base64: new Buffer("receipt", "utf-8").toString("base64")
47 | },
48 | bypassAppStoreValidation: true
49 | }
50 | }, function(err, res, body){
51 | if (typeof body != "object") {
52 | fail("Body is not an object");
53 | done();
54 | } else {
55 | expect(body.__type).toEqual("File");
56 | const url = body.url;
57 | request.get({
58 | url: url
59 | }, function(err, res, body) {
60 | expect(body).toEqual("download_file");
61 | done();
62 | });
63 | }
64 | });
65 | });
66 |
67 | it("should fail for missing receipt", (done) => {
68 | request.post({
69 | headers: {
70 | 'X-Parse-Application-Id': 'test',
71 | 'X-Parse-REST-API-Key': 'rest'},
72 | url: 'http://localhost:8378/1/validate_purchase',
73 | json: true,
74 | body: {
75 | productIdentifier: "a-product",
76 | bypassAppStoreValidation: true
77 | }
78 | }, function(err, res, body){
79 | if (typeof body != "object") {
80 | fail("Body is not an object");
81 | done();
82 | } else {
83 | expect(body.code).toEqual(Parse.Error.INVALID_JSON);
84 | done();
85 | }
86 | });
87 | });
88 |
89 | it("should fail for missing product identifier", (done) => {
90 | request.post({
91 | headers: {
92 | 'X-Parse-Application-Id': 'test',
93 | 'X-Parse-REST-API-Key': 'rest'},
94 | url: 'http://localhost:8378/1/validate_purchase',
95 | json: true,
96 | body: {
97 | receipt: {
98 | __type: "Bytes",
99 | base64: new Buffer("receipt", "utf-8").toString("base64")
100 | },
101 | bypassAppStoreValidation: true
102 | }
103 | }, function(err, res, body){
104 | if (typeof body != "object") {
105 | fail("Body is not an object");
106 | done();
107 | } else {
108 | expect(body.code).toEqual(Parse.Error.INVALID_JSON);
109 | done();
110 | }
111 | });
112 | });
113 |
114 | it("should bypass appstore validation and not find product", (done) => {
115 |
116 | request.post({
117 | headers: {
118 | 'X-Parse-Application-Id': 'test',
119 | 'X-Parse-REST-API-Key': 'rest'},
120 | url: 'http://localhost:8378/1/validate_purchase',
121 | json: true,
122 | body: {
123 | productIdentifier: "another-product",
124 | receipt: {
125 | __type: "Bytes",
126 | base64: new Buffer("receipt", "utf-8").toString("base64")
127 | },
128 | bypassAppStoreValidation: true
129 | }
130 | }, function(err, res, body){
131 | if (typeof body != "object") {
132 | fail("Body is not an object");
133 | done();
134 | } else {
135 | expect(body.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
136 | expect(body.error).toEqual('Object not found.');
137 | done();
138 | }
139 | });
140 | });
141 |
142 | it("should fail at appstore validation", (done) => {
143 |
144 | request.post({
145 | headers: {
146 | 'X-Parse-Application-Id': 'test',
147 | 'X-Parse-REST-API-Key': 'rest'},
148 | url: 'http://localhost:8378/1/validate_purchase',
149 | json: true,
150 | body: {
151 | productIdentifier: "a-product",
152 | receipt: {
153 | __type: "Bytes",
154 | base64: new Buffer("receipt", "utf-8").toString("base64")
155 | },
156 | }
157 | }, function(err, res, body){
158 | if (typeof body != "object") {
159 | fail("Body is not an object");
160 | } else {
161 | expect(body.status).toBe(21002);
162 | expect(body.error).toBe('The data in the receipt-data property was malformed or missing.');
163 | }
164 | done();
165 | });
166 | });
167 |
168 | it("should not create a _Product", (done) => {
169 | var product = new Parse.Object("_Product");
170 | product.save().then(function(){
171 | fail("Should not be able to save");
172 | done();
173 | }, function(err){
174 | expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE);
175 | done();
176 | })
177 | });
178 |
179 | it("should be able to update a _Product", (done) => {
180 | var query = new Parse.Query("_Product");
181 | query.first().then(function(product){
182 | product.set("title", "a new title");
183 | return product.save();
184 | }).then(function(productAgain){
185 | expect(productAgain.get('downloadName')).toEqual(productAgain.get('download').name());
186 | expect(productAgain.get("title")).toEqual("a new title");
187 | done();
188 | }).fail(function(err){
189 | fail(JSON.stringify(err));
190 | done();
191 | });
192 | });
193 |
194 | it("should not be able to remove a require key in a _Product", (done) => {
195 | var query = new Parse.Query("_Product");
196 | query.first().then(function(product){
197 | product.unset("title");
198 | return product.save();
199 | }).then(function(productAgain){
200 | fail("Should not succeed");
201 | done();
202 | }).fail(function(err){
203 | expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE);
204 | expect(err.message).toEqual("title is required.");
205 | done();
206 | });
207 | });
208 |
209 | });
210 |
--------------------------------------------------------------------------------
/src/Adapters/Logger/FileLoggerAdapter.js:
--------------------------------------------------------------------------------
1 | // Logger
2 | //
3 | // Wrapper around Winston logging library with custom query
4 | //
5 | // expected log entry to be in the shape of:
6 | // {"level":"info","message":"Your Message","timestamp":"2016-02-04T05:59:27.412Z"}
7 | //
8 | import { LoggerAdapter } from './LoggerAdapter';
9 | import winston from 'winston';
10 | import fs from 'fs';
11 | import { Parse } from 'parse/node';
12 |
13 | const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
14 | const CACHE_TIME = 1000 * 60;
15 |
16 | let LOGS_FOLDER = './logs/';
17 |
18 | if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
19 | LOGS_FOLDER = './test_logs/'
20 | }
21 |
22 | let currentDate = new Date();
23 |
24 | let simpleCache = {
25 | timestamp: null,
26 | from: null,
27 | until: null,
28 | order: null,
29 | data: [],
30 | level: 'info',
31 | };
32 |
33 | // returns Date object rounded to nearest day
34 | let _getNearestDay = (date) => {
35 | return new Date(date.getFullYear(), date.getMonth(), date.getDate());
36 | }
37 |
38 | // returns Date object of previous day
39 | let _getPrevDay = (date) => {
40 | return new Date(date - MILLISECONDS_IN_A_DAY);
41 | }
42 |
43 | // returns the iso formatted file name
44 | let _getFileName = () => {
45 | return _getNearestDay(currentDate).toISOString()
46 | }
47 |
48 | // check for valid cache when both from and util match.
49 | // cache valid for up to 1 minute
50 | let _hasValidCache = (from, until, level) => {
51 | if (String(from) === String(simpleCache.from) &&
52 | String(until) === String(simpleCache.until) &&
53 | new Date() - simpleCache.timestamp < CACHE_TIME &&
54 | level === simpleCache.level) {
55 | return true;
56 | }
57 | return false;
58 | }
59 |
60 | // renews transports to current date
61 | let _renewTransports = ({infoLogger, errorLogger, logsFolder}) => {
62 | if (infoLogger) {
63 | infoLogger.add(winston.transports.File, {
64 | filename: logsFolder + _getFileName() + '.info',
65 | name: 'info-file',
66 | level: 'info'
67 | });
68 | }
69 | if (errorLogger) {
70 | errorLogger.add(winston.transports.File, {
71 | filename: logsFolder + _getFileName() + '.error',
72 | name: 'error-file',
73 | level: 'error'
74 | });
75 | }
76 | };
77 |
78 | // check that log entry has valid time stamp based on query
79 | let _isValidLogEntry = (from, until, entry) => {
80 | var _entry = JSON.parse(entry),
81 | timestamp = new Date(_entry.timestamp);
82 | return timestamp >= from && timestamp <= until
83 | ? true
84 | : false
85 | };
86 |
87 | // ensure that file name is up to date
88 | let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => {
89 | if (_getNearestDay(currentDate) !== _getNearestDay(new Date())) {
90 | currentDate = new Date();
91 | if (infoLogger) {
92 | infoLogger.remove('info-file');
93 | }
94 | if (errorLogger) {
95 | errorLogger.remove('error-file');
96 | }
97 | _renewTransports({infoLogger, errorLogger, logsFolder});
98 | }
99 | }
100 |
101 | export class FileLoggerAdapter extends LoggerAdapter {
102 | constructor(options = {}) {
103 | super();
104 |
105 | this._logsFolder = options.logsFolder || LOGS_FOLDER;
106 |
107 | // check logs folder exists
108 | if (!fs.existsSync(this._logsFolder)) {
109 | fs.mkdirSync(this._logsFolder);
110 | }
111 |
112 | this._errorLogger = new (winston.Logger)({
113 | exitOnError: false,
114 | transports: [
115 | new (winston.transports.File)({
116 | filename: this._logsFolder + _getFileName() + '.error',
117 | name: 'error-file',
118 | level: 'error'
119 | })
120 | ]
121 | });
122 |
123 | this._infoLogger = new (winston.Logger)({
124 | exitOnError: false,
125 | transports: [
126 | new (winston.transports.File)({
127 | filename: this._logsFolder + _getFileName() + '.info',
128 | name: 'info-file',
129 | level: 'info'
130 | })
131 | ]
132 | });
133 | }
134 |
135 | info() {
136 | _verifyTransports({infoLogger: this._infoLogger, logsFolder: this._logsFolder});
137 | return this._infoLogger.info.apply(undefined, arguments);
138 | }
139 |
140 | error() {
141 | _verifyTransports({errorLogger: this._errorLogger, logsFolder: this._logsFolder});
142 | return this._errorLogger.error.apply(undefined, arguments);
143 | }
144 |
145 | // custom query as winston is currently limited
146 | query(options, callback) {
147 | if (!options) {
148 | options = {};
149 | }
150 | // defaults to 7 days prior
151 | let from = options.from || new Date(Date.now() - (7 * MILLISECONDS_IN_A_DAY));
152 | let until = options.until || new Date();
153 | let size = options.size || 10;
154 | let order = options.order || 'desc';
155 | let level = options.level || 'info';
156 | let roundedUntil = _getNearestDay(until);
157 | let roundedFrom = _getNearestDay(from);
158 |
159 | if (_hasValidCache(roundedFrom, roundedUntil, level)) {
160 | let logs = [];
161 | if (order !== simpleCache.order) {
162 | // reverse order of data
163 | simpleCache.data.forEach((entry) => {
164 | logs.unshift(entry);
165 | });
166 | } else {
167 | logs = simpleCache.data;
168 | }
169 | callback(logs.slice(0, size));
170 | return;
171 | }
172 |
173 | let curDate = roundedUntil;
174 | let curSize = 0;
175 | let method = order === 'desc' ? 'push' : 'unshift';
176 | let files = [];
177 | let promises = [];
178 |
179 | // current a batch call, all files with valid dates are read
180 | while (curDate >= from) {
181 | files[method](this._logsFolder + curDate.toISOString() + '.' + level);
182 | curDate = _getPrevDay(curDate);
183 | }
184 |
185 | // read each file and split based on newline char.
186 | // limitation is message cannot contain newline
187 | // TODO: strip out delimiter from logged message
188 | files.forEach(function(file, i) {
189 | let promise = new Parse.Promise();
190 | fs.readFile(file, 'utf8', function(err, data) {
191 | if (err) {
192 | promise.resolve([]);
193 | } else {
194 | let results = data.split('\n').filter((value) => {
195 | return value.trim() !== '';
196 | });
197 | promise.resolve(results);
198 | }
199 | });
200 | promises[method](promise);
201 | });
202 |
203 | Parse.Promise.when(promises).then((results) => {
204 | let logs = [];
205 | results.forEach(function(logEntries, i) {
206 | logEntries.forEach(function(entry) {
207 | if (_isValidLogEntry(from, until, entry)) {
208 | logs[method](JSON.parse(entry));
209 | }
210 | });
211 | });
212 | simpleCache = {
213 | timestamp: new Date(),
214 | from: roundedFrom,
215 | until: roundedUntil,
216 | data: logs,
217 | order,
218 | level,
219 | };
220 | callback(logs.slice(0, size));
221 | });
222 | }
223 | }
224 |
225 | export default FileLoggerAdapter;
226 |
--------------------------------------------------------------------------------
/spec/RestCreate.spec.js:
--------------------------------------------------------------------------------
1 | // These tests check the "create" functionality of the REST API.
2 | var auth = require('../src/Auth');
3 | var cache = require('../src/cache');
4 | var Config = require('../src/Config');
5 | var DatabaseAdapter = require('../src/DatabaseAdapter');
6 | var Parse = require('parse/node').Parse;
7 | var rest = require('../src/rest');
8 | var request = require('request');
9 |
10 | var config = new Config('test');
11 | var database = DatabaseAdapter.getDatabaseConnection('test');
12 |
13 | describe('rest create', () => {
14 | it('handles _id', (done) => {
15 | rest.create(config, auth.nobody(config), 'Foo', {}).then(() => {
16 | return database.mongoFind('Foo', {});
17 | }).then((results) => {
18 | expect(results.length).toEqual(1);
19 | var obj = results[0];
20 | expect(typeof obj._id).toEqual('string');
21 | expect(obj.objectId).toBeUndefined();
22 | done();
23 | });
24 | });
25 |
26 | it('handles array, object, date', (done) => {
27 | var obj = {
28 | array: [1, 2, 3],
29 | object: {foo: 'bar'},
30 | date: Parse._encode(new Date()),
31 | };
32 | rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
33 | return database.mongoFind('MyClass', {}, {});
34 | }).then((results) => {
35 | expect(results.length).toEqual(1);
36 | var mob = results[0];
37 | expect(mob.array instanceof Array).toBe(true);
38 | expect(typeof mob.object).toBe('object');
39 | expect(mob.date instanceof Date).toBe(true);
40 | done();
41 | });
42 | });
43 |
44 | it('handles user signup', (done) => {
45 | var user = {
46 | username: 'asdf',
47 | password: 'zxcv',
48 | foo: 'bar',
49 | };
50 | rest.create(config, auth.nobody(config), '_User', user)
51 | .then((r) => {
52 | expect(Object.keys(r.response).length).toEqual(3);
53 | expect(typeof r.response.objectId).toEqual('string');
54 | expect(typeof r.response.createdAt).toEqual('string');
55 | expect(typeof r.response.sessionToken).toEqual('string');
56 | done();
57 | });
58 | });
59 |
60 | it('handles anonymous user signup', (done) => {
61 | var data1 = {
62 | authData: {
63 | anonymous: {
64 | id: '00000000-0000-0000-0000-000000000001'
65 | }
66 | }
67 | };
68 | var data2 = {
69 | authData: {
70 | anonymous: {
71 | id: '00000000-0000-0000-0000-000000000002'
72 | }
73 | }
74 | };
75 | var username1;
76 | rest.create(config, auth.nobody(config), '_User', data1)
77 | .then((r) => {
78 | expect(typeof r.response.objectId).toEqual('string');
79 | expect(typeof r.response.createdAt).toEqual('string');
80 | expect(typeof r.response.sessionToken).toEqual('string');
81 | return rest.create(config, auth.nobody(config), '_User', data1);
82 | }).then((r) => {
83 | expect(typeof r.response.objectId).toEqual('string');
84 | expect(typeof r.response.createdAt).toEqual('string');
85 | expect(typeof r.response.username).toEqual('string');
86 | expect(typeof r.response.updatedAt).toEqual('string');
87 | username1 = r.response.username;
88 | return rest.create(config, auth.nobody(config), '_User', data2);
89 | }).then((r) => {
90 | expect(typeof r.response.objectId).toEqual('string');
91 | expect(typeof r.response.createdAt).toEqual('string');
92 | expect(typeof r.response.sessionToken).toEqual('string');
93 | return rest.create(config, auth.nobody(config), '_User', data2);
94 | }).then((r) => {
95 | expect(typeof r.response.objectId).toEqual('string');
96 | expect(typeof r.response.createdAt).toEqual('string');
97 | expect(typeof r.response.username).toEqual('string');
98 | expect(typeof r.response.updatedAt).toEqual('string');
99 | expect(r.response.username).not.toEqual(username1);
100 | done();
101 | });
102 | });
103 |
104 | it('handles no anonymous users config', (done) => {
105 | var NoAnnonConfig = Object.assign({}, config, {enableAnonymousUsers: false});
106 | var data1 = {
107 | authData: {
108 | anonymous: {
109 | id: '00000000-0000-0000-0000-000000000001'
110 | }
111 | }
112 | };
113 | rest.create(NoAnnonConfig, auth.nobody(NoAnnonConfig), '_User', data1).then(() => {
114 | fail("Should throw an error");
115 | done();
116 | }, (err) => {
117 | expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE);
118 | expect(err.message).toEqual('This authentication method is unsupported.');
119 | done();
120 | })
121 | });
122 |
123 | it('test facebook signup and login', (done) => {
124 | var data = {
125 | authData: {
126 | facebook: {
127 | id: '8675309',
128 | access_token: 'jenny'
129 | }
130 | }
131 | };
132 | rest.create(config, auth.nobody(config), '_User', data)
133 | .then((r) => {
134 | expect(typeof r.response.objectId).toEqual('string');
135 | expect(typeof r.response.createdAt).toEqual('string');
136 | expect(typeof r.response.sessionToken).toEqual('string');
137 | return rest.create(config, auth.nobody(config), '_User', data);
138 | }).then((r) => {
139 | expect(typeof r.response.objectId).toEqual('string');
140 | expect(typeof r.response.createdAt).toEqual('string');
141 | expect(typeof r.response.username).toEqual('string');
142 | expect(typeof r.response.updatedAt).toEqual('string');
143 | done();
144 | });
145 | });
146 |
147 | it('stores pointers with a _p_ prefix', (done) => {
148 | var obj = {
149 | foo: 'bar',
150 | aPointer: {
151 | __type: 'Pointer',
152 | className: 'JustThePointer',
153 | objectId: 'qwerty'
154 | }
155 | };
156 | rest.create(config, auth.nobody(config), 'APointerDarkly', obj)
157 | .then((r) => {
158 | return database.mongoFind('APointerDarkly', {});
159 | }).then((results) => {
160 | expect(results.length).toEqual(1);
161 | var output = results[0];
162 | expect(typeof output._id).toEqual('string');
163 | expect(typeof output._p_aPointer).toEqual('string');
164 | expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
165 | expect(output.aPointer).toBeUndefined();
166 | done();
167 | });
168 | });
169 |
170 | it("cannot set objectId", (done) => {
171 | var headers = {
172 | 'Content-Type': 'application/octet-stream',
173 | 'X-Parse-Application-Id': 'test',
174 | 'X-Parse-REST-API-Key': 'rest'
175 | };
176 | request.post({
177 | headers: headers,
178 | url: 'http://localhost:8378/1/classes/TestObject',
179 | body: JSON.stringify({
180 | 'foo': 'bar',
181 | 'objectId': 'hello'
182 | })
183 | }, (error, response, body) => {
184 | var b = JSON.parse(body);
185 | expect(b.code).toEqual(105);
186 | expect(b.error).toEqual('objectId is an invalid field name.');
187 | done();
188 | });
189 | });
190 |
191 | });
192 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://travis-ci.org/ParsePlatform/parse-server)
4 | [](https://codecov.io/github/ParsePlatform/parse-server?branch=master)
5 | [](https://www.npmjs.com/package/parse-server)
6 |
7 | Parse Server is an [open source version of the Parse backend](http://blog.parse.com/announcements/introducing-parse-server-and-the-database-migration-tool/) that can be deployed to any infrastructure that can run Node.js.
8 |
9 | Parse Server works with the Express web application framework. It can be added to existing web applications, or run by itself.
10 |
11 | ## Getting Started
12 |
13 | We have provided a basic [Node.js application](https://github.com/ParsePlatform/parse-server-example) that uses the Parse Server module on Express and can be easily deployed using any of the following buttons:
14 |
15 |
16 |
17 | ### Parse Server + Express
18 |
19 | You can also create an instance of Parse Server, and mount it on a new or existing Express website:
20 |
21 | ```js
22 | var express = require('express');
23 | var ParseServer = require('parse-server').ParseServer;
24 | var app = express();
25 |
26 | // Specify the connection string for your mongodb database
27 | // and the location to your Parse cloud code
28 | var api = new ParseServer({
29 | databaseURI: 'mongodb://localhost:27017/dev',
30 | cloud: '/home/myApp/cloud/main.js', // Provide an absolute path
31 | appId: 'myAppId',
32 | masterKey: 'myMasterKey', // Keep this key secret!
33 | fileKey: 'optionalFileKey',
34 | serverURL: 'http://localhost:1337/parse' // Don't forget to change to https if needed
35 | });
36 |
37 | // Serve the Parse API on the /parse URL prefix
38 | app.use('/parse', api);
39 |
40 | app.listen(1337, function() {
41 | console.log('parse-server-example running on port 1337.');
42 | });
43 | ```
44 |
45 | ### Standalone Parse Server
46 |
47 | Parse Server can also run as a standalone API server. The standalone Parse Server can be configured using [environment variables](#configuration). To start the server, just run `npm start`.
48 |
49 | You can also install Parse Server globally:
50 |
51 | `$ npm install -g parse-server`
52 |
53 | Now you can just run `$ parse-server` from your command line.
54 |
55 |
56 | ## Documentation
57 |
58 | The full documentation for Parse Server is available in the [wiki](https://github.com/ParsePlatform/parse-server/wiki). The [Parse Server guide](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide) is a good place to get started. If you're interested in developing for Parse Server, the [Development guide](https://github.com/ParsePlatform/parse-server/wiki/Development-Guide) will help you get set up.
59 |
60 | #### Migrating an Existing Parse App
61 |
62 | The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App).
63 |
64 | ### Configuration
65 |
66 | The following options can be passed to the `ParseServer` object during initialization. Alternatively, you can use the `PARSE_SERVER_OPTIONS` environment variable set to the JSON of your configuration.
67 |
68 | #### Basic options
69 |
70 | * `databaseURI` (required) - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`
71 | * `appId` (required) - The application id to host with this server instance
72 | * `masterKey` (required) - The master key to use for overriding ACL security
73 | * `cloud` - The absolute path to your cloud code main.js file
74 | * `fileKey` - For migrated apps, this is necessary to provide access to files already hosted on Parse.
75 | * `facebookAppIds` - An array of valid Facebook application IDs.
76 | * `serverURL` - URL which will be used by Cloud Code functions to make requests against.
77 | * `push` - Configuration options for APNS and GCM push. See the [wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
78 |
79 | #### Client key options
80 |
81 | The client keys used with Parse are no longer necessary with Parse Server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at initialization time. Setting any of these keys will require all requests to provide one of the configured keys.
82 |
83 | * `clientKey`
84 | * `javascriptKey`
85 | * `restAPIKey`
86 | * `dotNetKey`
87 |
88 | #### Advanced options
89 |
90 | * `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js))
91 | * `databaseAdapter` (unfinished) - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`)
92 | * `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))
93 | * `enableAnonymousUsers` - Defaults to true. Set to false to disable anonymous users.
94 | * `oauth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth).
95 |
96 | #### Using environment variables
97 |
98 | You may also configure the Parse Server using environment variables:
99 |
100 | ```js
101 | PARSE_SERVER_DATABASE_URI
102 | PARSE_SERVER_CLOUD_CODE_MAIN
103 | PARSE_SERVER_COLLECTION_PREFIX
104 | PARSE_SERVER_APPLICATION_ID // required
105 | PARSE_SERVER_MASTER_KEY // required
106 | PARSE_SERVER_CLIENT_KEY
107 | PARSE_SERVER_REST_API_KEY
108 | PARSE_SERVER_DOTNET_KEY
109 | PARSE_SERVER_JAVASCRIPT_KEY
110 | PARSE_SERVER_DOTNET_KEY
111 | PARSE_SERVER_FILE_KEY
112 | PARSE_SERVER_FACEBOOK_APP_IDS // string of comma separated list
113 |
114 | ```
115 |
116 | ## Contributing
117 |
118 | We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
119 |
--------------------------------------------------------------------------------
/src/middlewares.js:
--------------------------------------------------------------------------------
1 | var Parse = require('parse/node').Parse;
2 |
3 | var auth = require('./Auth');
4 | var cache = require('./cache');
5 | var Config = require('./Config');
6 |
7 | // Checks that the request is authorized for this app and checks user
8 | // auth too.
9 | // The bodyparser should run before this middleware.
10 | // Adds info to the request:
11 | // req.config - the Config for this app
12 | // req.auth - the Auth for this request
13 | function handleParseHeaders(req, res, next) {
14 | var mountPathLength = req.originalUrl.length - req.url.length;
15 | var mountPath = req.originalUrl.slice(0, mountPathLength);
16 | var mount = req.protocol + '://' + req.get('host') + mountPath;
17 |
18 | var info = {
19 | appId: req.get('X-Parse-Application-Id'),
20 | sessionToken: req.get('X-Parse-Session-Token'),
21 | masterKey: req.get('X-Parse-Master-Key'),
22 | installationId: req.get('X-Parse-Installation-Id'),
23 | clientKey: req.get('X-Parse-Client-Key'),
24 | javascriptKey: req.get('X-Parse-Javascript-Key'),
25 | dotNetKey: req.get('X-Parse-Windows-Key'),
26 | restAPIKey: req.get('X-Parse-REST-API-Key')
27 | };
28 |
29 | if (req.body) {
30 | // Unity SDK sends a _noBody key which needs to be removed.
31 | // Unclear at this point if action needs to be taken.
32 | delete req.body._noBody;
33 | }
34 |
35 | var fileViaJSON = false;
36 |
37 | if (!info.appId || !cache.apps[info.appId]) {
38 | // See if we can find the app id on the body.
39 | if (req.body instanceof Buffer) {
40 | // The only chance to find the app id is if this is a file
41 | // upload that actually is a JSON body. So try to parse it.
42 | req.body = JSON.parse(req.body);
43 | fileViaJSON = true;
44 | }
45 |
46 | if (req.body && req.body._ApplicationId
47 | && cache.apps[req.body._ApplicationId]
48 | && (
49 | !info.masterKey
50 | ||
51 | cache.apps[req.body._ApplicationId]['masterKey'] === info.masterKey)
52 | ) {
53 | info.appId = req.body._ApplicationId;
54 | info.javascriptKey = req.body._JavaScriptKey || '';
55 | delete req.body._ApplicationId;
56 | delete req.body._JavaScriptKey;
57 | // TODO: test that the REST API formats generated by the other
58 | // SDKs are handled ok
59 | if (req.body._ClientVersion) {
60 | info.clientVersion = req.body._ClientVersion;
61 | delete req.body._ClientVersion;
62 | }
63 | if (req.body._InstallationId) {
64 | info.installationId = req.body._InstallationId;
65 | delete req.body._InstallationId;
66 | }
67 | if (req.body._SessionToken) {
68 | info.sessionToken = req.body._SessionToken;
69 | delete req.body._SessionToken;
70 | }
71 | if (req.body._MasterKey) {
72 | info.masterKey = req.body._MasterKey;
73 | delete req.body._MasterKey;
74 | }
75 | } else {
76 | return invalidRequest(req, res);
77 | }
78 | }
79 |
80 | if (fileViaJSON) {
81 | // We need to repopulate req.body with a buffer
82 | var base64 = req.body.base64;
83 | req.body = new Buffer(base64, 'base64');
84 | }
85 |
86 | info.app = cache.apps[info.appId];
87 | req.config = new Config(info.appId, mount);
88 | req.database = req.config.database;
89 | req.info = info;
90 |
91 | var isMaster = (info.masterKey === req.config.masterKey);
92 |
93 | if (isMaster) {
94 | req.auth = new auth.Auth(req.config, true);
95 | next();
96 | return;
97 | }
98 |
99 | // Client keys are not required in parse-server, but if any have been configured in the server, validate them
100 | // to preserve original behavior.
101 | var keyRequired = (req.config.clientKey
102 | || req.config.javascriptKey
103 | || req.config.dotNetKey
104 | || req.config.restAPIKey);
105 | var keyHandled = false;
106 | if (keyRequired
107 | && ((info.clientKey && req.config.clientKey && info.clientKey === req.config.clientKey)
108 | || (info.javascriptKey && req.config.javascriptKey && info.javascriptKey === req.config.javascriptKey)
109 | || (info.dotNetKey && req.config.dotNetKey && info.dotNetKey === req.config.dotNetKey)
110 | || (info.restAPIKey && req.config.restAPIKey && info.restAPIKey === req.config.restAPIKey)
111 | )) {
112 | keyHandled = true;
113 | }
114 | if (keyRequired && !keyHandled) {
115 | return invalidRequest(req, res);
116 | }
117 |
118 | if (!info.sessionToken) {
119 | req.auth = new auth.Auth(req.config, false);
120 | next();
121 | return;
122 | }
123 |
124 | return auth.getAuthForSessionToken(
125 | req.config, info.sessionToken).then((auth) => {
126 | if (auth) {
127 | req.auth = auth;
128 | next();
129 | }
130 | }).catch((error) => {
131 | // TODO: Determine the correct error scenario.
132 | console.log(error);
133 | throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
134 | });
135 |
136 | }
137 |
138 | var allowCrossDomain = function(req, res, next) {
139 | res.header('Access-Control-Allow-Origin', '*');
140 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
141 | res.header('Access-Control-Allow-Headers', 'X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, Content-Type');
142 |
143 | // intercept OPTIONS method
144 | if ('OPTIONS' == req.method) {
145 | res.send(200);
146 | }
147 | else {
148 | next();
149 | }
150 | };
151 |
152 | var allowMethodOverride = function(req, res, next) {
153 | if (req.method === 'POST' && req.body._method) {
154 | req.originalMethod = req.method;
155 | req.method = req.body._method;
156 | delete req.body._method;
157 | }
158 | next();
159 | };
160 |
161 | var handleParseErrors = function(err, req, res, next) {
162 | if (err instanceof Parse.Error) {
163 | var httpStatus;
164 |
165 | // TODO: fill out this mapping
166 | switch (err.code) {
167 | case Parse.Error.INTERNAL_SERVER_ERROR:
168 | httpStatus = 500;
169 | break;
170 | case Parse.Error.OBJECT_NOT_FOUND:
171 | httpStatus = 404;
172 | break;
173 | default:
174 | httpStatus = 400;
175 | }
176 |
177 | res.status(httpStatus);
178 | res.json({code: err.code, error: err.message});
179 | } else {
180 | console.log('Uncaught internal server error.', err, err.stack);
181 | res.status(500);
182 | res.json({code: Parse.Error.INTERNAL_SERVER_ERROR,
183 | message: 'Internal server error.'});
184 | }
185 | };
186 |
187 | function enforceMasterKeyAccess(req, res, next) {
188 | if (!req.auth.isMaster) {
189 | res.status(403);
190 | res.end('{"error":"unauthorized: master key is required"}');
191 | return;
192 | }
193 | next();
194 | }
195 |
196 | function invalidRequest(req, res) {
197 | res.status(403);
198 | res.end('{"error":"unauthorized"}');
199 | }
200 |
201 | module.exports = {
202 | allowCrossDomain: allowCrossDomain,
203 | allowMethodOverride: allowMethodOverride,
204 | handleParseErrors: handleParseErrors,
205 | handleParseHeaders: handleParseHeaders,
206 | enforceMasterKeyAccess: enforceMasterKeyAccess
207 | };
208 |
--------------------------------------------------------------------------------
/spec/OneSignalPushAdapter.spec.js:
--------------------------------------------------------------------------------
1 |
2 | var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter');
3 | var classifyInstallations = require('../src/Adapters/Push/PushAdapterUtils').classifyInstallations;
4 | describe('OneSignalPushAdapter', () => {
5 | it('can be initialized', (done) => {
6 | // Make mock config
7 | var pushConfig = {
8 | oneSignalAppId:"APP ID",
9 | oneSignalApiKey:"API KEY"
10 | };
11 |
12 | var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
13 |
14 | var senderMap = oneSignalPushAdapter.senderMap;
15 |
16 | expect(senderMap.ios instanceof Function).toBe(true);
17 | expect(senderMap.android instanceof Function).toBe(true);
18 | done();
19 | });
20 |
21 | it('can get valid push types', (done) => {
22 | var oneSignalPushAdapter = new OneSignalPushAdapter();
23 |
24 | expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
25 | done();
26 | });
27 |
28 | it('can classify installation', (done) => {
29 | // Mock installations
30 | var validPushTypes = ['ios', 'android'];
31 | var installations = [
32 | {
33 | deviceType: 'android',
34 | deviceToken: 'androidToken'
35 | },
36 | {
37 | deviceType: 'ios',
38 | deviceToken: 'iosToken'
39 | },
40 | {
41 | deviceType: 'win',
42 | deviceToken: 'winToken'
43 | },
44 | {
45 | deviceType: 'android',
46 | deviceToken: undefined
47 | }
48 | ];
49 |
50 | var deviceMap = OneSignalPushAdapter.classifyInstallations(installations, validPushTypes);
51 | expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
52 | expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
53 | expect(deviceMap['win']).toBe(undefined);
54 | done();
55 | });
56 |
57 |
58 | it('can send push notifications', (done) => {
59 | var oneSignalPushAdapter = new OneSignalPushAdapter();
60 |
61 | // Mock android ios senders
62 | var androidSender = jasmine.createSpy('send')
63 | var iosSender = jasmine.createSpy('send')
64 |
65 | var senderMap = {
66 | ios: iosSender,
67 | android: androidSender
68 | };
69 | oneSignalPushAdapter.senderMap = senderMap;
70 |
71 | // Mock installations
72 | var installations = [
73 | {
74 | deviceType: 'android',
75 | deviceToken: 'androidToken'
76 | },
77 | {
78 | deviceType: 'ios',
79 | deviceToken: 'iosToken'
80 | },
81 | {
82 | deviceType: 'win',
83 | deviceToken: 'winToken'
84 | },
85 | {
86 | deviceType: 'android',
87 | deviceToken: undefined
88 | }
89 | ];
90 | var data = {};
91 |
92 | oneSignalPushAdapter.send(data, installations);
93 | // Check android sender
94 | expect(androidSender).toHaveBeenCalled();
95 | var args = androidSender.calls.first().args;
96 | expect(args[0]).toEqual(data);
97 | expect(args[1]).toEqual([
98 | makeDevice('androidToken')
99 | ]);
100 | // Check ios sender
101 | expect(iosSender).toHaveBeenCalled();
102 | args = iosSender.calls.first().args;
103 | expect(args[0]).toEqual(data);
104 | expect(args[1]).toEqual([
105 | makeDevice('iosToken')
106 | ]);
107 | done();
108 | });
109 |
110 | it("can send iOS notifications", (done) => {
111 | var oneSignalPushAdapter = new OneSignalPushAdapter();
112 | var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
113 | oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
114 |
115 | oneSignalPushAdapter.sendToAPNS({'data':{
116 | 'badge': 1,
117 | 'alert': "Example content",
118 | 'sound': "Example sound",
119 | 'content-available': 1,
120 | 'misc-data': 'Example Data'
121 | }},[{'deviceToken':'iosToken1'},{'deviceToken':'iosToken2'}])
122 |
123 | expect(sendToOneSignal).toHaveBeenCalled();
124 | var args = sendToOneSignal.calls.first().args;
125 | expect(args[0]).toEqual({
126 | 'ios_badgeType':'SetTo',
127 | 'ios_badgeCount':1,
128 | 'contents': { 'en':'Example content'},
129 | 'ios_sound': 'Example sound',
130 | 'content_available':true,
131 | 'data':{'misc-data':'Example Data'},
132 | 'include_ios_tokens':['iosToken1','iosToken2']
133 | })
134 | done();
135 | });
136 |
137 | it("can send Android notifications", (done) => {
138 | var oneSignalPushAdapter = new OneSignalPushAdapter();
139 | var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
140 | oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
141 |
142 | oneSignalPushAdapter.sendToGCM({'data':{
143 | 'title': 'Example title',
144 | 'alert': 'Example content',
145 | 'misc-data': 'Example Data'
146 | }},[{'deviceToken':'androidToken1'},{'deviceToken':'androidToken2'}])
147 |
148 | expect(sendToOneSignal).toHaveBeenCalled();
149 | var args = sendToOneSignal.calls.first().args;
150 | expect(args[0]).toEqual({
151 | 'contents': { 'en':'Example content'},
152 | 'title': {'en':'Example title'},
153 | 'data':{'misc-data':'Example Data'},
154 | 'include_android_reg_ids': ['androidToken1','androidToken2']
155 | })
156 | done();
157 | });
158 |
159 | it("can post the correct data", (done) => {
160 | var pushConfig = {
161 | oneSignalAppId:"APP ID",
162 | oneSignalApiKey:"API KEY"
163 | };
164 | var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
165 |
166 | var write = jasmine.createSpy('write');
167 | oneSignalPushAdapter.https = {
168 | 'request': function(a,b) {
169 | return {
170 | 'end':function(){},
171 | 'on':function(a,b){},
172 | 'write':write
173 | }
174 | }
175 | };
176 |
177 | var installations = [
178 | {
179 | deviceType: 'android',
180 | deviceToken: 'androidToken'
181 | },
182 | {
183 | deviceType: 'ios',
184 | deviceToken: 'iosToken'
185 | },
186 | {
187 | deviceType: 'win',
188 | deviceToken: 'winToken'
189 | },
190 | {
191 | deviceType: 'android',
192 | deviceToken: undefined
193 | }
194 | ];
195 |
196 | oneSignalPushAdapter.send({'data':{
197 | 'title': 'Example title',
198 | 'alert': 'Example content',
199 | 'content-available':1,
200 | 'misc-data': 'Example Data'
201 | }}, installations);
202 |
203 | expect(write).toHaveBeenCalled();
204 |
205 | // iOS
206 | args = write.calls.first().args;
207 | expect(args[0]).toEqual(JSON.stringify({
208 | 'contents': { 'en':'Example content'},
209 | 'content_available':true,
210 | 'data':{'title':'Example title','misc-data':'Example Data'},
211 | 'include_ios_tokens':['iosToken'],
212 | 'app_id':'APP ID'
213 | }));
214 |
215 | // Android
216 | args = write.calls.mostRecent().args;
217 | expect(args[0]).toEqual(JSON.stringify({
218 | 'contents': { 'en':'Example content'},
219 | 'title': {'en':'Example title'},
220 | 'data':{"content-available":1,'misc-data':'Example Data'},
221 | 'include_android_reg_ids':['androidToken'],
222 | 'app_id':'APP ID'
223 | }));
224 |
225 | done();
226 | });
227 |
228 | function makeDevice(deviceToken, appIdentifier) {
229 | return {
230 | deviceToken: deviceToken,
231 | appIdentifier: appIdentifier
232 | };
233 | }
234 |
235 | });
236 |
--------------------------------------------------------------------------------