├── 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 | ![Parse Server logo](.github/parse-server-logo.png?raw=true) 2 | 3 | [![Build Status](https://img.shields.io/travis/ParsePlatform/parse-server/master.svg?style=flat)](https://travis-ci.org/ParsePlatform/parse-server) 4 | [![Coverage Status](https://img.shields.io/codecov/c/github/ParsePlatform/parse-server/master.svg)](https://codecov.io/github/ParsePlatform/parse-server?branch=master) 5 | [![npm version](https://img.shields.io/npm/v/parse-server.svg?style=flat)](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 | --------------------------------------------------------------------------------