From c13b61f07ec1a83cdf766c19fbb7d3059cef61c2 Mon Sep 17 00:00:00 2001 From: Dmitry Chestnykh Date: Sat, 30 Jan 2016 09:02:02 +0100 Subject: [PATCH 01/16] Use CSPRNG to generate objectIds --- RestWrite.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/RestWrite.js b/RestWrite.js index 6e63a324a3..418e077bd5 100644 --- a/RestWrite.js +++ b/RestWrite.js @@ -2,6 +2,7 @@ // that writes to the database. // This could be either a "create" or an "update". +var crypto = require('crypto'); var deepcopy = require('deepcopy'); var rack = require('hat').rack(); @@ -702,15 +703,18 @@ RestWrite.prototype.objectId = function() { return this.data.objectId || this.query.objectId; }; -// Returns a string that's usable as an object id. -// Probably unique. Good enough? Probably! +// Returns a unique string that's usable as an object id. function newObjectId() { var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789'); var objectId = ''; - for (var i = 0; i < 10; ++i) { - objectId += chars[Math.floor(Math.random() * chars.length)]; + var bytes = crypto.randomBytes(10); + for (var i = 0; i < bytes.length; ++i) { + // Note: there is a slight modulo bias, because chars length + // of 62 doesn't divide the number of all bytes (256) evenly. + // It is acceptable for our purposes. + objectId += chars[bytes.readUInt8(i) % chars.length]; } return objectId; } From 03302459e243462858104b3d5a7e19713dc9c723 Mon Sep 17 00:00:00 2001 From: Natan Rolnik Date: Sun, 31 Jan 2016 02:52:16 +0200 Subject: [PATCH 02/16] Converts httpRequest body to string if needed --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 79a321986f..e2115f4098 100644 --- a/index.js +++ b/index.js @@ -150,6 +150,9 @@ function addParseCloud() { options.uri = options.url; delete options.url; } + if (typeof options.body === 'object') { + options.body = JSON.stringify(options.body); + } request(options, (error, response, body) => { if (error) { if (callbacks.error) { @@ -178,4 +181,3 @@ function getClassName(parseClass) { module.exports = { ParseServer: ParseServer }; - From 1328270014bb607e4a620dbafd812512472e1436 Mon Sep 17 00:00:00 2001 From: Konstantin Simakov Date: Mon, 1 Feb 2016 12:48:39 +0500 Subject: [PATCH 03/16] Fix #119: Empty find options for GET requests --- classes.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/classes.js b/classes.js index 92d2a220ee..8a2f4624ae 100644 --- a/classes.js +++ b/classes.js @@ -10,35 +10,36 @@ var router = new PromiseRouter(); // Returns a promise that resolves to a {response} object. function handleFind(req) { + var body = req.method === 'GET' ? req.query : req.body; var options = {}; - if (req.body.skip) { - options.skip = Number(req.body.skip); + if (body.skip) { + options.skip = Number(body.skip); } - if (req.body.limit) { - options.limit = Number(req.body.limit); + if (body.limit) { + options.limit = Number(body.limit); } - if (req.body.order) { - options.order = String(req.body.order); + if (body.order) { + options.order = String(body.order); } - if (req.body.count) { + if (body.count) { options.count = true; } - if (typeof req.body.keys == 'string') { - options.keys = req.body.keys; + if (typeof body.keys == 'string') { + options.keys = body.keys; } - if (req.body.include) { - options.include = String(req.body.include); + if (body.include) { + options.include = String(body.include); } - if (req.body.redirectClassNameForKey) { - options.redirectClassNameForKey = String(req.body.redirectClassNameForKey); + if (body.redirectClassNameForKey) { + options.redirectClassNameForKey = String(body.redirectClassNameForKey); } - if(typeof req.body.where === 'string') { - req.body.where = JSON.parse(req.body.where); + if(typeof body.where === 'string') { + body.where = JSON.parse(body.where); } return rest.find(req.config, req.auth, - req.params.className, req.body.where, options) + req.params.className, body.where, options) .then((response) => { return {response: response}; }); From 234c40553003ccac6481760ecad505243926a6c1 Mon Sep 17 00:00:00 2001 From: Konstantin Simakov Date: Mon, 1 Feb 2016 13:36:42 +0500 Subject: [PATCH 04/16] Fix #119: remove 'GET' condition and replace it with assign --- classes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes.js b/classes.js index 8a2f4624ae..dc33eab09f 100644 --- a/classes.js +++ b/classes.js @@ -10,7 +10,7 @@ var router = new PromiseRouter(); // Returns a promise that resolves to a {response} object. function handleFind(req) { - var body = req.method === 'GET' ? req.query : req.body; + var body = Object.assign(req.body, req.query); var options = {}; if (body.skip) { options.skip = Number(body.skip); From e8e7f4ba6b3b25e75ca85871a8ad5d65e020c681 Mon Sep 17 00:00:00 2001 From: Jordan Date: Mon, 1 Feb 2016 00:40:01 -0800 Subject: [PATCH 05/16] fix untransform null objects --- spec/transform.spec.js | 83 ++++++++++++++++++++++++++++++++++-------- transform.js | 5 +++ 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/spec/transform.spec.js b/spec/transform.spec.js index 559d787b50..b404eec0bd 100644 --- a/spec/transform.spec.js +++ b/spec/transform.spec.js @@ -2,16 +2,18 @@ var transform = require('../transform'); -var dummyConfig = { - schema: { +var dummySchema = { data: {}, getExpectedType: function(className, key) { if (key == 'userPointer') { return '*_User'; + } else if (key == 'picture') { + return 'file'; + } else if (key == 'location') { + return 'geopoint'; } return; } - } }; @@ -19,7 +21,7 @@ describe('transformCreate', () => { it('a basic number', (done) => { var input = {five: 5}; - var output = transform.transformCreate(dummyConfig, null, input); + var output = transform.transformCreate(dummySchema, null, input); jequal(input, output); done(); }); @@ -29,7 +31,7 @@ describe('transformCreate', () => { createdAt: "2015-10-06T21:24:50.332Z", updatedAt: "2015-10-06T21:24:50.332Z" }; - var output = transform.transformCreate(dummyConfig, null, input); + var output = transform.transformCreate(dummySchema, null, input); expect(output._created_at instanceof Date).toBe(true); expect(output._updated_at instanceof Date).toBe(true); done(); @@ -41,21 +43,21 @@ describe('transformCreate', () => { objectId: 'myId', className: 'Blah', }; - var out = transform.transformCreate(dummyConfig, null, {pointers: [pointer]}); + var out = transform.transformCreate(dummySchema, null, {pointers: [pointer]}); jequal([pointer], out.pointers); done(); }); it('a delete op', (done) => { var input = {deleteMe: {__op: 'Delete'}}; - var output = transform.transformCreate(dummyConfig, null, input); + var output = transform.transformCreate(dummySchema, null, input); jequal(output, {}); done(); }); it('basic ACL', (done) => { var input = {ACL: {'0123': {'read': true, 'write': true}}}; - var output = transform.transformCreate(dummyConfig, null, input); + var output = transform.transformCreate(dummySchema, null, input); // This just checks that it doesn't crash, but it should check format. done(); }); @@ -63,7 +65,7 @@ describe('transformCreate', () => { describe('transformWhere', () => { it('objectId', (done) => { - var out = transform.transformWhere(dummyConfig, null, {objectId: 'foo'}); + var out = transform.transformWhere(dummySchema, null, {objectId: 'foo'}); expect(out._id).toEqual('foo'); done(); }); @@ -72,7 +74,7 @@ describe('transformWhere', () => { var input = { objectId: {'$in': ['one', 'two', 'three']}, }; - var output = transform.transformWhere(dummyConfig, null, input); + var output = transform.transformWhere(dummySchema, null, input); jequal(input.objectId, output._id); done(); }); @@ -81,17 +83,66 @@ describe('transformWhere', () => { describe('untransformObject', () => { it('built-in timestamps', (done) => { var input = {createdAt: new Date(), updatedAt: new Date()}; - var output = transform.untransformObject(dummyConfig, null, input); + var output = transform.untransformObject(dummySchema, null, input); expect(typeof output.createdAt).toEqual('string'); expect(typeof output.updatedAt).toEqual('string'); done(); }); + + it('pointer', (done) => { + var input = {_p_userPointer: '_User$123'}; + var output = transform.untransformObject(dummySchema, null, input); + expect(typeof output.userPointer).toEqual('object'); + expect(output.userPointer).toEqual( + {__type: 'Pointer', className: '_User', objectId: '123'} + ); + done(); + }); + + it('null pointer', (done) => { + var input = {_p_userPointer: null}; + var output = transform.untransformObject(dummySchema, null, input); + expect(output.userPointer).toBeUndefined(); + done(); + }); + + it('file', (done) => { + var input = {picture: 'pic.jpg'}; + var output = transform.untransformObject(dummySchema, null, input); + expect(typeof output.picture).toEqual('object'); + expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'}); + done(); + }); + + it('null file', (done) => { + var input = {picture: null}; + var output = transform.untransformObject(dummySchema, null, input); + expect(output.picture).toBeUndefined(); + done(); + }); + + it('geopoint', (done) => { + var input = {location: [180, -180]}; + var output = transform.untransformObject(dummySchema, null, input); + expect(typeof output.location).toEqual('object'); + expect(output.location).toEqual( + {__type: 'GeoPoint', longitude: 180, latitude: -180} + ); + done(); + }); + + it('null geopoint', (done) => { + var input = {location: null}; + var output = transform.untransformObject(dummySchema, null, input); + expect(output.location).toBeUndefined(); + done(); + }); }); describe('transformKey', () => { it('throws out _password', (done) => { try { - transform.transformKey(dummyConfig, '_User', '_password'); + transform.transformKey(dummySchema, '_User', '_password'); fail('should have thrown'); } catch (e) { done(); @@ -105,7 +156,7 @@ describe('transform schema key changes', () => { var input = { somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'} }; - var output = transform.transformCreate(dummyConfig, null, input); + var output = transform.transformCreate(dummySchema, null, input); expect(typeof output._p_somePointer).toEqual('string'); expect(output._p_somePointer).toEqual('Micro$oft'); done(); @@ -115,7 +166,7 @@ describe('transform schema key changes', () => { var input = { userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'} }; - var output = transform.transformCreate(dummyConfig, null, input); + var output = transform.transformCreate(dummySchema, null, input); expect(typeof output._p_userPointer).toEqual('string'); expect(output._p_userPointer).toEqual('_User$qwerty'); done(); @@ -128,7 +179,7 @@ describe('transform schema key changes', () => { "Kevin": { "write": true } } }; - var output = transform.transformCreate(dummyConfig, null, input); + var output = transform.transformCreate(dummySchema, null, input); expect(typeof output._rperm).toEqual('object'); expect(typeof output._wperm).toEqual('object'); expect(output.ACL).toBeUndefined(); @@ -142,7 +193,7 @@ describe('transform schema key changes', () => { _rperm: ["*"], _wperm: ["Kevin"] }; - var output = transform.untransformObject(dummyConfig, null, input); + var output = transform.untransformObject(dummySchema, null, input); expect(typeof output.ACL).toEqual('object'); expect(output._rperm).toBeUndefined(); expect(output._wperm).toBeUndefined(); diff --git a/transform.js b/transform.js index 7e19ba7035..f7da102085 100644 --- a/transform.js +++ b/transform.js @@ -676,6 +676,9 @@ function untransformObject(schema, className, mongoObject) { console.log('Found a pointer in a non-pointer column, dropping it.', className, key); break; } + if (mongoObject[key] === null) { + break; + } var objData = mongoObject[key].split('$'); var newClass = (expected ? expected.substring(1) : objData[0]); if (objData[0] !== newClass) { @@ -689,6 +692,8 @@ function untransformObject(schema, className, mongoObject) { break; } else if (key[0] == '_' && key != '__type') { throw ('bad key in untransform: ' + key); + } else if (mongoObject[key] === null) { + break; } else { var expected = schema.getExpectedType(className, key); if (expected == 'file') { From 956fd0840fd777ecc7f7c2447d1a066c5f34ba45 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 2 Feb 2016 03:09:57 +1300 Subject: [PATCH 06/16] remove trailing spaces --- spec/ParseUser.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index cd7e850f74..d7eb2ac84b 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1574,7 +1574,7 @@ describe('Parse.User testing', () => { var sessionToken = null; Parse.Promise.as().then(function() { - return Parse.User.signUp("fosco", "parse"); + return Parse.User.signUp("fosco", "parse"); }).then(function(newUser) { equal(Parse.User.current(), newUser); sessionToken = newUser.getSessionToken(); From 07b0235349f74213ce8b5d7f99fbd6329cb43e6a Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Mon, 1 Feb 2016 12:22:41 -0500 Subject: [PATCH 07/16] installationId only gets set on the session if passed in to the request Signed-off-by: Alexander Mays --- users.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/users.js b/users.js index 642474b867..b18aad808d 100644 --- a/users.js +++ b/users.js @@ -70,9 +70,13 @@ function handleLogIn(req) { 'authProvider': 'password' }, restricted: false, - expiresAt: Parse._encode(expiresAt).iso, - installationId: req.info.installationId + expiresAt: Parse._encode(expiresAt).iso }; + + if (req.info.installationId) { + sessionData.installationId = req.info.installationId + } + var create = new RestWrite(req.config, Auth.master(req.config), '_Session', null, sessionData); return create.execute(); From 26dfdd84a6d4d797f83479d0435aa84c81ff3c99 Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Mon, 1 Feb 2016 12:44:18 -0500 Subject: [PATCH 08/16] Correctly passes the ParseDate rather than iso string when creating a new session Signed-off-by: Alexander Mays --- users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users.js b/users.js index 642474b867..f84ad5d4fb 100644 --- a/users.js +++ b/users.js @@ -70,7 +70,7 @@ function handleLogIn(req) { 'authProvider': 'password' }, restricted: false, - expiresAt: Parse._encode(expiresAt).iso, + expiresAt: Parse._encode(expiresAt), installationId: req.info.installationId }; var create = new RestWrite(req.config, Auth.master(req.config), From 1cddbb0a37aa546afd9cf50605103df0db250f59 Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Sun, 31 Jan 2016 16:54:16 -0800 Subject: [PATCH 09/16] :checkered_flag: Add Windows Support (bcrypt > bcrypt-node) - This commit replaces bcrypt with bcrypt-node, which has the same functionality as bcrypy - except that it is a pure Node implementation. This change is required to run parse-server on Windows (one can get bcrypt to compile on Windows, but it requires a few Gigabytes of dependencies). --- crypto.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crypto.js b/crypto.js index fdbcdf9ab7..907f33be85 100644 --- a/crypto.js +++ b/crypto.js @@ -1,6 +1,6 @@ // Tools for encrypting and decrypting passwords. // Basically promise-friendly wrappers for bcrypt. -var bcrypt = require('bcrypt'); +var bcrypt = require('bcrypt-nodejs'); // Returns a promise for a hashed password string. function hash(password) { diff --git a/package.json b/package.json index 1f6560f3bf..bed46504fb 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "license": "BSD-3-Clause", "dependencies": { - "bcrypt": "~0.8", + "bcrypt-nodejs": "0.0.3", "body-parser": "~1.12.4", "deepcopy": "^0.5.0", "express": "~4.2.x", From 40bfc4ae95526071e3400b9c6a2c341e0472f135 Mon Sep 17 00:00:00 2001 From: Jamie Chapman Date: Mon, 1 Feb 2016 19:11:02 +0000 Subject: [PATCH 10/16] Added multi-app support in DatabaseAdapter.js --- DatabaseAdapter.js | 12 ++++++++++-- index.js | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/DatabaseAdapter.js b/DatabaseAdapter.js index 82efb8fd73..4967d5665d 100644 --- a/DatabaseAdapter.js +++ b/DatabaseAdapter.js @@ -20,6 +20,7 @@ var adapter = ExportAdapter; var cache = require('./cache'); var dbConnections = {}; var databaseURI = 'mongodb://localhost:27017/parse'; +var appDatabaseURIs = {}; function setAdapter(databaseAdapter) { adapter = databaseAdapter; @@ -29,11 +30,17 @@ function setDatabaseURI(uri) { databaseURI = uri; } +function setAppDatabaseURI(appId, uri) { + appDatabaseURIs[appId] = uri; +} + function getDatabaseConnection(appId) { if (dbConnections[appId]) { return dbConnections[appId]; } - dbConnections[appId] = new adapter(databaseURI, { + + var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI); + dbConnections[appId] = new adapter(dbURI, { collectionPrefix: cache.apps[appId]['collectionPrefix'] }); dbConnections[appId].connect(); @@ -44,5 +51,6 @@ module.exports = { dbConnections: dbConnections, getDatabaseConnection: getDatabaseConnection, setAdapter: setAdapter, - setDatabaseURI: setDatabaseURI + setDatabaseURI: setDatabaseURI, + setAppDatabaseURI: setAppDatabaseURI }; diff --git a/index.js b/index.js index e2115f4098..59b5155c0f 100644 --- a/index.js +++ b/index.js @@ -47,7 +47,7 @@ function ParseServer(args) { FilesAdapter.setAdapter(args.filesAdapter); } if (args.databaseURI) { - DatabaseAdapter.setDatabaseURI(args.databaseURI); + DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI); } if (args.cloud) { addParseCloud(); From cb1079a6d3535e2c8abb4aeed59716308c69104f Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Mon, 1 Feb 2016 11:24:26 -0800 Subject: [PATCH 11/16] Addressed bugs with bcrypt-nodejs and changed crypto.js to password.js --- RestWrite.js | 4 ++-- crypto.js => password.js | 2 +- spec/ParseUser.spec.js | 4 ++-- users.js | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename crypto.js => password.js (91%) diff --git a/RestWrite.js b/RestWrite.js index 229fb665ac..ea7b2225e2 100644 --- a/RestWrite.js +++ b/RestWrite.js @@ -9,7 +9,7 @@ var rack = require('hat').rack(); var Auth = require('./Auth'); var cache = require('./cache'); var Config = require('./Config'); -var crypto = require('./crypto'); +var passwordCrypto = require('./password'); var facebook = require('./facebook'); var Parse = require('parse/node'); var triggers = require('./triggers'); @@ -300,7 +300,7 @@ RestWrite.prototype.transformUser = function() { if (this.query) { this.storage['clearSessions'] = true; } - return crypto.hash(this.data.password).then((hashedPassword) => { + return passwordCrypto.hash(this.data.password).then((hashedPassword) => { this.data._hashed_password = hashedPassword; delete this.data.password; }); diff --git a/crypto.js b/password.js similarity index 91% rename from crypto.js rename to password.js index 907f33be85..f1154c96e6 100644 --- a/crypto.js +++ b/password.js @@ -5,7 +5,7 @@ var bcrypt = require('bcrypt-nodejs'); // Returns a promise for a hashed password string. function hash(password) { return new Promise(function(fulfill, reject) { - bcrypt.hash(password, 8, function(err, hashedPassword) { + bcrypt.hash(password, null, null, function(err, hashedPassword) { if (err) { reject(err); } else { diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index d7eb2ac84b..458b43eef4 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -6,7 +6,7 @@ // Tests that involve sending password reset emails. var request = require('request'); -var crypto = require('../crypto'); +var passwordCrypto = require('../password'); describe('Parse.User testing', () => { it("user sign up class method", (done) => { @@ -1560,7 +1560,7 @@ describe('Parse.User testing', () => { it('password format matches hosted parse', (done) => { var hashed = '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie'; - crypto.compare('test', hashed) + passwordCrypto.compare('test', hashed) .then((pass) => { expect(pass).toBe(true); done(); diff --git a/users.js b/users.js index b18aad808d..76deec7c9a 100644 --- a/users.js +++ b/users.js @@ -5,7 +5,7 @@ var Parse = require('parse/node').Parse; var rack = require('hat').rack(); var Auth = require('./Auth'); -var crypto = require('./crypto'); +var passwordCrypto = require('./password'); var facebook = require('./facebook'); var PromiseRouter = require('./PromiseRouter'); var rest = require('./rest'); @@ -45,7 +45,7 @@ function handleLogIn(req) { 'Invalid username/password.'); } user = results[0]; - return crypto.compare(req.body.password, user.password); + return passwordCrypto.compare(req.body.password, user.password); }).then((correct) => { if (!correct) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, From 86e42976f91212dfd3e4d4e81da22447873c0937 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Mon, 1 Feb 2016 11:35:20 -0800 Subject: [PATCH 12/16] Some updates to fix tests --- spec/transform.spec.js | 13 ------------- transform.js | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/spec/transform.spec.js b/spec/transform.spec.js index b404eec0bd..c581c5d6c3 100644 --- a/spec/transform.spec.js +++ b/spec/transform.spec.js @@ -114,13 +114,6 @@ describe('untransformObject', () => { done(); }); - it('null file', (done) => { - var input = {picture: null}; - var output = transform.untransformObject(dummySchema, null, input); - expect(output.picture).toBeUndefined(); - done(); - }); - it('geopoint', (done) => { var input = {location: [180, -180]}; var output = transform.untransformObject(dummySchema, null, input); @@ -131,12 +124,6 @@ describe('untransformObject', () => { done(); }); - it('null geopoint', (done) => { - var input = {location: null}; - var output = transform.untransformObject(dummySchema, null, input); - expect(output.location).toBeUndefined(); - done(); - }); }); describe('transformKey', () => { diff --git a/transform.js b/transform.js index f7da102085..01346d13a1 100644 --- a/transform.js +++ b/transform.js @@ -692,8 +692,8 @@ function untransformObject(schema, className, mongoObject) { break; } else if (key[0] == '_' && key != '__type') { throw ('bad key in untransform: ' + key); - } else if (mongoObject[key] === null) { - break; + //} else if (mongoObject[key] === null) { + //break; } else { var expected = schema.getExpectedType(className, key); if (expected == 'file') { From 3f2124410a0a884c4b65f535d0ba8ae66375dd5e Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Mon, 1 Feb 2016 11:44:03 -0800 Subject: [PATCH 13/16] Adding CONTRIBUTING.md and Open Code of Conduct. --- CONTRIBUTING.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..faaae4e388 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +### Contributing to Parse Server + +#### Pull Requests Welcome! + +We really want Parse to be yours, to see it grow and thrive in the open source community. + +##### Please Do's + +* Please write tests to cover new methods. +* Please run the tests and make sure you didn't break anything. + +##### Code of Conduct + +This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code. +[code-of-conduct]: http://todogroup.org/opencodeofconduct/#Parse Server/fjm@fb.com + + From 05c2246f7bf75fb454d0f2034ea7859840581163 Mon Sep 17 00:00:00 2001 From: Patrick Pelletier Date: Sun, 31 Jan 2016 22:27:33 -0800 Subject: [PATCH 14/16] Add support for saving files to AWS S3 --- FilesAdapter.js | 1 + GridStoreAdapter.js | 12 ++++++- S3Adapter.js | 79 +++++++++++++++++++++++++++++++++++++++++++++ files.js | 6 +--- package.json | 1 + 5 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 S3Adapter.js diff --git a/FilesAdapter.js b/FilesAdapter.js index 7b952ed031..427e20d9bb 100644 --- a/FilesAdapter.js +++ b/FilesAdapter.js @@ -5,6 +5,7 @@ // Adapter classes must implement the following functions: // * create(config, filename, data) // * get(config, filename) +// * location(config, req, filename) // // Default is GridStoreAdapter, which requires mongo // and for the API server to be using the ExportAdapter diff --git a/GridStoreAdapter.js b/GridStoreAdapter.js index 3168de066a..0d1e896578 100644 --- a/GridStoreAdapter.js +++ b/GridStoreAdapter.js @@ -4,6 +4,7 @@ // Requires the database adapter to be based on mongoclient var GridStore = require('mongodb').GridStore; +var path = require('path'); // For a given config object, filename, and data, store a file // Returns a promise @@ -32,7 +33,16 @@ function get(config, filename) { }); } +// Generates and returns the location of a file stored in GridStore for the +// given request and filename +function location(config, req, filename) { + return (req.protocol + '://' + req.get('host') + + path.dirname(req.originalUrl) + '/' + req.config.applicationId + + '/' + encodeURIComponent(filename)); +} + module.exports = { create: create, - get: get + get: get, + location: location }; diff --git a/S3Adapter.js b/S3Adapter.js new file mode 100644 index 0000000000..e4930e0391 --- /dev/null +++ b/S3Adapter.js @@ -0,0 +1,79 @@ +// S3Adapter +// +// Stores Parse files in AWS S3. + +var AWS = require('aws-sdk'); +var path = require('path'); + +var DEFAULT_REGION = "us-east-1"; +var DEFAULT_BUCKET = "parse-files"; + +// Creates an S3 session. +// Providing AWS access and secret keys is mandatory +// Region and bucket will use sane defaults if omitted +function S3Adapter(accessKey, secretKey, options) { + options = options || {}; + + this.region = options.region || DEFAULT_REGION; + this.bucket = options.bucket || DEFAULT_BUCKET; + this.bucketPrefix = options.bucketPrefix || ""; + this.directAccess = options.directAccess || false; + + s3Options = { + accessKeyId: accessKey, + secretAccessKey: secretKey, + params: {Bucket: this.bucket} + }; + AWS.config.region = this.region; + this.s3 = new AWS.S3(s3Options); +} + +// For a given config object, filename, and data, store a file in S3 +// Returns a promise containing the S3 object creation response +S3Adapter.prototype.create = function(config, filename, data) { + var self = this; + var params = { + Key: self.bucketPrefix + filename, + Body: data, + }; + if (self.directAccess) { + params.ACL = "public-read" + } + + return new Promise(function(resolve, reject) { + self.s3.upload(params, function(err, data) { + if (err !== null) return reject(err); + resolve(data); + }); + }); +} + +// Search for and return a file if found by filename +// Returns a promise that succeeds with the buffer result from S3 +S3Adapter.prototype.get = function(config, filename) { + var self = this; + var params = {Key: self.bucketPrefix + filename}; + + return new Promise(function(resolve, reject) { + self.s3.getObject(params, function(err, data) { + if (err !== null) return reject(err); + resolve(data.Body); + }); + }); +} + +// Generates and returns the location of a file stored in S3 for the given request and +// filename +// The location is the direct S3 link if the option is set, otherwise we serve +// the file through parse-server +S3Adapter.prototype.location = function(config, req, filename) { + if (this.directAccess) { + return ('https://' + this.bucket + '.s3.amazonaws.com' + '/' + + this.bucketPrefix + filename); + } + return (req.protocol + '://' + req.get('host') + + path.dirname(req.originalUrl) + '/' + req.config.applicationId + + '/' + encodeURIComponent(filename)); +} + +module.exports = S3Adapter; diff --git a/files.js b/files.js index 2c36e3417c..e2575a5d7e 100644 --- a/files.js +++ b/files.js @@ -7,7 +7,6 @@ var bodyParser = require('body-parser'), middlewares = require('./middlewares.js'), mime = require('mime'), Parse = require('parse/node').Parse, - path = require('path'), rack = require('hat').rack(); var router = express.Router(); @@ -44,10 +43,7 @@ var processCreate = function(req, res, next) { FilesAdapter.getAdapter().create(req.config, filename, req.body) .then(() => { res.status(201); - var location = (req.protocol + '://' + req.get('host') + - path.dirname(req.originalUrl) + '/' + - req.config.applicationId + '/' + - encodeURIComponent(filename)); + var location = FilesAdapter.getAdapter().location(req.config, req, filename); res.set('Location', location); res.json({ url: location, name: filename }); }).catch((error) => { diff --git a/package.json b/package.json index bed46504fb..c8dd3b94b1 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "license": "BSD-3-Clause", "dependencies": { + "aws-sdk": "~2.2.33", "bcrypt-nodejs": "0.0.3", "body-parser": "~1.12.4", "deepcopy": "^0.5.0", From 2008c4dce90c1a4ca2fe425a6cbfe01579445315 Mon Sep 17 00:00:00 2001 From: Patrick Pelletier Date: Mon, 1 Feb 2016 10:30:01 -0800 Subject: [PATCH 15/16] Use ES6 => instead of self = this ack --- S3Adapter.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/S3Adapter.js b/S3Adapter.js index e4930e0391..aeea44efb6 100644 --- a/S3Adapter.js +++ b/S3Adapter.js @@ -31,17 +31,16 @@ function S3Adapter(accessKey, secretKey, options) { // For a given config object, filename, and data, store a file in S3 // Returns a promise containing the S3 object creation response S3Adapter.prototype.create = function(config, filename, data) { - var self = this; var params = { - Key: self.bucketPrefix + filename, + Key: this.bucketPrefix + filename, Body: data, }; - if (self.directAccess) { + if (this.directAccess) { params.ACL = "public-read" } - - return new Promise(function(resolve, reject) { - self.s3.upload(params, function(err, data) { + + return new Promise((resolve, reject) => { + this.s3.upload(params, function(err, data) { if (err !== null) return reject(err); resolve(data); }); @@ -51,11 +50,10 @@ S3Adapter.prototype.create = function(config, filename, data) { // Search for and return a file if found by filename // Returns a promise that succeeds with the buffer result from S3 S3Adapter.prototype.get = function(config, filename) { - var self = this; - var params = {Key: self.bucketPrefix + filename}; + var params = {Key: this.bucketPrefix + filename}; - return new Promise(function(resolve, reject) { - self.s3.getObject(params, function(err, data) { + return new Promise((resolve, reject) => { + this.s3.getObject(params, (err, data) => { if (err !== null) return reject(err); resolve(data.Body); }); From 00540cb2deb38ac4ee73028c00deca60350e1109 Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Mon, 1 Feb 2016 12:44:18 -0500 Subject: [PATCH 16/16] Correctly passes the ParseDate rather than iso string when creating a new session Signed-off-by: Alexander Mays --- users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users.js b/users.js index 76deec7c9a..007808543e 100644 --- a/users.js +++ b/users.js @@ -70,7 +70,7 @@ function handleLogIn(req) { 'authProvider': 'password' }, restricted: false, - expiresAt: Parse._encode(expiresAt).iso + expiresAt: Parse._encode(expiresAt) }; if (req.info.installationId) {