From a75376523ca180bb6bae5effe8e45e492c2b6eea Mon Sep 17 00:00:00 2001 From: Wes Thomas Date: Wed, 10 Feb 2016 18:42:21 -0500 Subject: [PATCH 1/8] file DELETE support --- spec/ParseFile.spec.js | 89 ++++++++++++++++++++++++++ src/Adapters/Files/FilesAdapter.js | 2 + src/Adapters/Files/GridStoreAdapter.js | 11 ++++ src/Adapters/Files/S3Adapter.js | 14 ++++ src/Controllers/FilesController.js | 26 ++++++++ 5 files changed, 142 insertions(+) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index b65d8f34..7287dd14 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -33,6 +33,95 @@ describe('Parse.File testing', () => { }); }); + it('supports REST end-to-end file create, read, delete, read', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/testfile.txt', + body: 'check one two', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_testfile.txt$/); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*testfile.txt$/); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('check one two'); + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'test' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toEqual(200); + request.get({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: b.url + }, (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toEqual(404); + done(); + }); + }); + }); + }); + }); + + it('blocks file deletions with missing or incorrect master-key header', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/thefile.jpg', + body: 'the file body' + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/); + // missing X-Parse-Master-Key header + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + var del_b = JSON.parse(body); + expect(response.statusCode).toEqual(400); + expect(del_b.code).toEqual(119); + // incorrect X-Parse-Master-Key header + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'tryagain' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + var del_b2 = JSON.parse(body); + expect(response.statusCode).toEqual(400); + expect(del_b2.code).toEqual(119); + done(); + }); + }); + }); + }); + it('handles other filetypes', done => { var headers = { 'Content-Type': 'image/jpeg', diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index 9daed517..a1d5955f 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -14,6 +14,8 @@ export class FilesAdapter { createFile(config, filename, data) { } + deleteFile(config, filename) { } + getFileData(config, filename) { } getFileLocation(config, filename) { } diff --git a/src/Adapters/Files/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js index 8c95319d..21934c9a 100644 --- a/src/Adapters/Files/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -20,6 +20,17 @@ export class GridStoreAdapter extends FilesAdapter { }); } + deleteFile(config, filename) { + return config.database.connect().then(() => { + let gridStore = new GridStore(config.database.db, filename, 'w'); + return gridStore.open(); + }).then((gridStore) => { + return gridStore.unlink(); + }).then((gridStore) => { + return gridStore.close(); + }); + } + getFileData(config, filename) { return config.database.connect().then(() => { return GridStore.exist(config.database.db, filename); diff --git a/src/Adapters/Files/S3Adapter.js b/src/Adapters/Files/S3Adapter.js index 2c892246..b33b66f1 100644 --- a/src/Adapters/Files/S3Adapter.js +++ b/src/Adapters/Files/S3Adapter.js @@ -56,6 +56,20 @@ export class S3Adapter extends FilesAdapter { }); } + deleteFile(config, filename) { + return new Promise((resolve, reject) => { + let params = { + Key: this._bucketPrefix + filename + }; + this._s3Client.deleteObject(params, (err, data) =>{ + if(err !== null) { + return reject(err); + } + resolve(data); + }); + }); + } + // Search for and return a file if found by filename // Returns a promise that succeeds with the buffer result from S3 getFileData(config, filename) { diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 47454f07..321042b9 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -74,6 +74,26 @@ export class FilesController { }; } + deleteHandler() { + return (req, res, next) => { + // enforce use of master key for file deletions + if(!req.auth.isMaster){ + next(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, + 'Master key required for file deletion.')); + return; + } + + this._filesAdapter.deleteFile(req.config, req.params.filename).then(() => { + res.status(200); + // TODO: return useful JSON here? + res.end(); + }).catch((error) => { + next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR, + 'Could not delete file.')); + }); + }; + } + /** * Find file references in REST-format object and adds the url key * with the current mount point and app id. @@ -119,6 +139,12 @@ export class FilesController { this.createHandler() ); + router.delete('/files/:filename', + Middlewares.allowCrossDomain, + Middlewares.handleParseHeaders, + this.deleteHandler() + ); + return router; } } From ab841b5ab475b32a11549c942a9e4663f63b9163 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Thu, 11 Feb 2016 20:01:14 -0800 Subject: [PATCH 2/8] Refactor and deduplicate logic in UsersRouter. --- src/Routers/UsersRouter.js | 163 ++++++++++++++++++++++++++++ src/index.js | 3 +- src/users.js | 212 ------------------------------------- 3 files changed, 165 insertions(+), 213 deletions(-) create mode 100644 src/Routers/UsersRouter.js delete mode 100644 src/users.js diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js new file mode 100644 index 00000000..4f22e07a --- /dev/null +++ b/src/Routers/UsersRouter.js @@ -0,0 +1,163 @@ +// These methods handle the User-related routes. + +import hat from 'hat'; +import deepcopy from 'deepcopy'; + +import ClassesRouter from './ClassesRouter'; +import PromiseRouter from '../PromiseRouter'; +import rest from '../rest'; +import Auth from '../Auth'; +import passwordCrypto from '../password'; +import RestWrite from '../RestWrite'; + +const rack = hat.rack(); + +export class UsersRouter extends ClassesRouter { + handleFind(req) { + req.params.className = '_User'; + return super.handleFind(req); + } + + handleGet(req) { + req.params.className = '_User'; + return super.handleGet(req); + } + + handleCreate(req) { + let data = deepcopy(req.body); + data.installationId = req.info.installationId; + req.body = data; + req.params.className = '_User'; + return super.handleCreate(req); + } + + handleUpdate(req) { + req.params.className = '_User'; + return super.handleUpdate(req); + } + + handleDelete(req) { + req.params.className = '_User'; + return super.handleDelete(req); + } + + handleMe(req) { + if (!req.info || !req.info.sessionToken) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.'); + } + return rest.find(req.config, Auth.master(req.config), '_Session', + { _session_token: req.info.sessionToken }, + { include: 'user' }) + .then((response) => { + if (!response.results || + response.results.length == 0 || + !response.results[0].user) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.'); + } else { + let user = response.results[0].user; + return { response: user }; + } + }); + } + + handleLogIn(req) { + // Use query parameters instead if provided in url + if (!req.body.username && req.query.username) { + req.body = req.query; + } + + // TODO: use the right error codes / descriptions. + if (!req.body.username) { + throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'username is required.'); + } + if (!req.body.password) { + throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'password is required.'); + } + + let user; + return req.database.find('_User', { username: req.body.username }) + .then((results) => { + if (!results.length) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); + } + user = results[0]; + return passwordCrypto.compare(req.body.password, user.password); + }).then((correct) => { + if (!correct) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); + } + + let token = 'r:' + rack(); + user.sessionToken = token; + delete user.password; + + req.config.filesController.expandFilesInObject(req.config, user); + + let expiresAt = new Date(); + expiresAt.setFullYear(expiresAt.getFullYear() + 1); + + let sessionData = { + sessionToken: token, + user: { + __type: 'Pointer', + className: '_User', + objectId: user.objectId + }, + createdWith: { + 'action': 'login', + 'authProvider': 'password' + }, + restricted: false, + expiresAt: Parse._encode(expiresAt) + }; + + if (req.info.installationId) { + sessionData.installationId = req.info.installationId + } + + let create = new RestWrite(req.config, Auth.master(req.config), '_Session', null, sessionData); + return create.execute(); + }).then(() => { + return { response: user }; + }); + } + + handleLogOut(req) { + let success = {response: {}}; + if (req.info && req.info.sessionToken) { + return rest.find(req.config, Auth.master(req.config), '_Session', + { _session_token: req.info.sessionToken } + ).then((records) => { + if (records.results && records.results.length) { + return rest.del(req.config, Auth.master(req.config), '_Session', + records.results[0].objectId + ).then(() => { + return Promise.resolve(success); + }); + } + return Promise.resolve(success); + }); + } + return Promise.resolve(success); + } + + getExpressRouter() { + let router = new PromiseRouter(); + router.route('GET', '/users', req => { return this.handleFind(req); }); + router.route('POST', '/users', req => { return this.handleCreate(req); }); + router.route('GET', '/users/:objectId', req => { return this.handleGet(req); }); + router.route('PUT', '/users/:objectId', req => { return this.handleUpdate(req); }); + router.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); }); + router.route('GET', '/users/me', req => { return this.handleMe(req); }); + router.route('GET', '/login', req => { return this.handleLogIn(req); }); + router.route('POST', '/logout', req => { return this.handleLogOut(req); }); + router.route('POST', '/requestPasswordReset', () => { + throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'This path is not implemented yet.'); + }); + return router; + } +} + +export default UsersRouter; diff --git a/src/index.js b/src/index.js index c2993400..fcc573e2 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ import { PushController } from './Controllers/PushController'; import { ClassesRouter } from './Routers/ClassesRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; +import { UsersRouter } from './Routers/UsersRouter'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -129,7 +130,7 @@ function ParseServer(args) { let routers = [ new ClassesRouter().getExpressRouter(), - require('./users'), + new UsersRouter().getExpressRouter(), require('./sessions'), require('./roles'), require('./analytics'), diff --git a/src/users.js b/src/users.js deleted file mode 100644 index 4205c666..00000000 --- a/src/users.js +++ /dev/null @@ -1,212 +0,0 @@ -// These methods handle the User-related routes. - -var mongodb = require('mongodb'); -var Parse = require('parse/node').Parse; -var rack = require('hat').rack(); - -var Auth = require('./Auth'); -var passwordCrypto = require('./password'); -var facebook = require('./facebook'); -var PromiseRouter = require('./PromiseRouter'); -var rest = require('./rest'); -var RestWrite = require('./RestWrite'); -var deepcopy = require('deepcopy'); - -var router = new PromiseRouter(); - -// Returns a promise for a {status, response, location} object. -function handleCreate(req) { - var data = deepcopy(req.body); - data.installationId = req.info.installationId; - return rest.create(req.config, req.auth, - '_User', data); -} - -// Returns a promise for a {response} object. -function handleLogIn(req) { - - // Use query parameters instead if provided in url - if (!req.body.username && req.query.username) { - req.body = req.query; - } - - // TODO: use the right error codes / descriptions. - if (!req.body.username) { - throw new Parse.Error(Parse.Error.USERNAME_MISSING, - 'username is required.'); - } - if (!req.body.password) { - throw new Parse.Error(Parse.Error.PASSWORD_MISSING, - 'password is required.'); - } - - var user; - return req.database.find('_User', {username: req.body.username}) - .then((results) => { - if (!results.length) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Invalid username/password.'); - } - user = results[0]; - return passwordCrypto.compare(req.body.password, user.password); - }).then((correct) => { - if (!correct) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Invalid username/password.'); - } - var token = 'r:' + rack(); - user.sessionToken = token; - delete user.password; - - req.config.filesController.expandFilesInObject(req.config, user); - - var expiresAt = new Date(); - expiresAt.setFullYear(expiresAt.getFullYear() + 1); - - var sessionData = { - sessionToken: token, - user: { - __type: 'Pointer', - className: '_User', - objectId: user.objectId - }, - createdWith: { - 'action': 'login', - 'authProvider': 'password' - }, - restricted: false, - expiresAt: Parse._encode(expiresAt) - }; - - 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(); - }).then(() => { - return {response: user}; - }); -} - -// Returns a promise that resolves to a {response} object. -// TODO: share code with ClassesRouter.js -function handleFind(req) { - var options = {}; - if (req.body.skip) { - options.skip = Number(req.body.skip); - } - if (req.body.limit) { - options.limit = Number(req.body.limit); - } - if (req.body.order) { - options.order = String(req.body.order); - } - if (req.body.count) { - options.count = true; - } - if (typeof req.body.keys == 'string') { - options.keys = req.body.keys; - } - if (req.body.include) { - options.include = String(req.body.include); - } - if (req.body.redirectClassNameForKey) { - options.redirectClassNameForKey = String(req.body.redirectClassNameForKey); - } - - return rest.find(req.config, req.auth, - '_User', req.body.where, options) - .then((response) => { - return {response: response}; - }); - -} - -// Returns a promise for a {response} object. -function handleGet(req) { - return rest.find(req.config, req.auth, '_User', - {objectId: req.params.objectId}) - .then((response) => { - if (!response.results || response.results.length == 0) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.'); - } else { - return {response: response.results[0]}; - } - }); -} - -function handleMe(req) { - if (!req.info || !req.info.sessionToken) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.'); - } - return rest.find(req.config, Auth.master(req.config), '_Session', - {_session_token: req.info.sessionToken}, - {include: 'user'}) - .then((response) => { - if (!response.results || response.results.length == 0 || - !response.results[0].user) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.'); - } else { - var user = response.results[0].user; - return {response: user}; - } - }); -} - -function handleDelete(req) { - return rest.del(req.config, req.auth, - req.params.className, req.params.objectId) - .then(() => { - return {response: {}}; - }); -} - -function handleLogOut(req) { - var success = {response: {}}; - if (req.info && req.info.sessionToken) { - return rest.find(req.config, Auth.master(req.config), '_Session', - {_session_token: req.info.sessionToken} - ).then((records) => { - if (records.results && records.results.length) { - return rest.del(req.config, Auth.master(req.config), '_Session', - records.results[0].objectId - ).then(() => { - return Promise.resolve(success); - }); - } - return Promise.resolve(success); - }); - } - return Promise.resolve(success); -} - -function handleUpdate(req) { - return rest.update(req.config, req.auth, '_User', - req.params.objectId, req.body) - .then((response) => { - return {response: response}; - }); -} - -function notImplementedYet(req) { - throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, - 'This path is not implemented yet.'); -} - -router.route('POST', '/users', handleCreate); -router.route('GET', '/login', handleLogIn); -router.route('POST', '/logout', handleLogOut); -router.route('GET', '/users/me', handleMe); -router.route('GET', '/users/:objectId', handleGet); -router.route('PUT', '/users/:objectId', handleUpdate); -router.route('GET', '/users', handleFind); -router.route('DELETE', '/users/:objectId', handleDelete); - -router.route('POST', '/requestPasswordReset', notImplementedYet); - -module.exports = router; From 99ac6c11610ea42bde65aac4b1b496d53c6e3bea Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Thu, 11 Feb 2016 20:40:15 -0800 Subject: [PATCH 3/8] Refactor and deduplicate logic in SessionsRouter. --- src/Routers/SessionsRouter.js | 63 ++++++++++++++++++++++ src/index.js | 3 +- src/sessions.js | 98 ----------------------------------- 3 files changed, 65 insertions(+), 99 deletions(-) create mode 100644 src/Routers/SessionsRouter.js delete mode 100644 src/sessions.js diff --git a/src/Routers/SessionsRouter.js b/src/Routers/SessionsRouter.js new file mode 100644 index 00000000..ecffd80a --- /dev/null +++ b/src/Routers/SessionsRouter.js @@ -0,0 +1,63 @@ + +import ClassesRouter from './ClassesRouter'; +import PromiseRouter from '../PromiseRouter'; +import rest from '../rest'; +import Auth from '../Auth'; + +export class SessionsRouter extends ClassesRouter { + handleFind(req) { + req.params.className = '_Session'; + return super.handleFind(req); + } + + handleGet(req) { + req.params.className = '_Session'; + return super.handleGet(req); + } + + handleCreate(req) { + req.params.className = '_Session'; + return super.handleCreate(req); + } + + handleUpdate(req) { + req.params.className = '_Session'; + return super.handleUpdate(req); + } + + handleDelete(req) { + req.params.className = '_Session'; + return super.handleDelete(req); + } + + handleMe(req) { + // TODO: Verify correct behavior + if (!req.info || !req.info.sessionToken) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, + 'Session token required.'); + } + return rest.find(req.config, Auth.master(req.config), '_Session', { _session_token: req.info.sessionToken }) + .then((response) => { + if (!response.results || response.results.length == 0) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, + 'Session token not found.'); + } + return { + response: response.results[0] + }; + }); + } + + getExpressRouter() { + let router = new PromiseRouter(); + router.route('GET','/sessions/me', req => { return this.handleMe(req); }); + router.route('GET', '/sessions', req => { return this.handleFind(req); }); + router.route('GET', '/sessions/:objectId', req => { return this.handleGet(req); }); + router.route('POST', '/sessions', req => { return this.handleCreate(req); }); + router.route('PUT', '/sessions/:objectId', req => { return this.handleUpdate(req); }); + router.route('DELETE', '/sessions/:objectId', req => { return this.handleDelete(req); }); + return router; + } +} + +export default SessionsRouter; diff --git a/src/index.js b/src/index.js index fcc573e2..9d9910bd 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,7 @@ import { PushController } from './Controllers/PushController'; import { ClassesRouter } from './Routers/ClassesRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; import { UsersRouter } from './Routers/UsersRouter'; +import { SessionsRouter } from './Routers/SessionsRouter'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -131,7 +132,7 @@ function ParseServer(args) { let routers = [ new ClassesRouter().getExpressRouter(), new UsersRouter().getExpressRouter(), - require('./sessions'), + new SessionsRouter().getExpressRouter(), require('./roles'), require('./analytics'), new InstallationsRouter().getExpressRouter(), diff --git a/src/sessions.js b/src/sessions.js deleted file mode 100644 index b979de45..00000000 --- a/src/sessions.js +++ /dev/null @@ -1,98 +0,0 @@ -// sessions.js - -var Auth = require('./Auth'), - Parse = require('parse/node').Parse, - PromiseRouter = require('./PromiseRouter'), - rest = require('./rest'); - -var router = new PromiseRouter(); - -function handleCreate(req) { - return rest.create(req.config, req.auth, - '_Session', req.body); -} - -function handleUpdate(req) { - return rest.update(req.config, req.auth, '_Session', - req.params.objectId, req.body) - .then((response) => { - return {response: response}; - }); -} - -function handleDelete(req) { - return rest.del(req.config, req.auth, - '_Session', req.params.objectId) - .then(() => { - return {response: {}}; - }); -} - -function handleGet(req) { - return rest.find(req.config, req.auth, '_Session', - {objectId: req.params.objectId}) - .then((response) => { - if (!response.results || response.results.length == 0) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.'); - } else { - return {response: response.results[0]}; - } - }); -} - -function handleFind(req) { - var options = {}; - if (req.body.skip) { - options.skip = Number(req.body.skip); - } - if (req.body.limit) { - options.limit = Number(req.body.limit); - } - if (req.body.order) { - options.order = String(req.body.order); - } - if (req.body.count) { - options.count = true; - } - if (typeof req.body.keys == 'string') { - options.keys = req.body.keys; - } - if (req.body.include) { - options.include = String(req.body.include); - } - - return rest.find(req.config, req.auth, - '_Session', req.body.where, options) - .then((response) => { - return {response: response}; - }); -} - -function handleMe(req) { - // TODO: Verify correct behavior - if (!req.info || !req.info.sessionToken) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token required.'); - } - return rest.find(req.config, Auth.master(req.config), '_Session', - { _session_token: req.info.sessionToken}) - .then((response) => { - if (!response.results || response.results.length == 0) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token not found.'); - } - return { - response: response.results[0] - }; - }); -} - -router.route('POST','/sessions', handleCreate); -router.route('GET','/sessions/me', handleMe); -router.route('GET','/sessions/:objectId', handleGet); -router.route('PUT','/sessions/:objectId', handleUpdate); -router.route('GET','/sessions', handleFind); -router.route('DELETE','/sessions/:objectId', handleDelete); - -module.exports = router; From b2570a9af85a131f4f86f3683c603133f54557b7 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Thu, 11 Feb 2016 23:17:20 -0800 Subject: [PATCH 4/8] Update style in InstallationsRouter. --- src/Routers/InstallationsRouter.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index 033366b7..fca703e9 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -51,12 +51,12 @@ export class InstallationsRouter extends ClassesRouter { } getExpressRouter() { - var router = new PromiseRouter(); - router.route('GET','/installations', (req) => { return this.handleFind(req); }); - router.route('GET','/installations/:objectId', (req) => { return this.handleGet(req); }); - router.route('POST','/installations', (req) => { return this.handleCreate(req); }); - router.route('PUT','/installations/:objectId', (req) => { return this.handleUpdate(req); }); - router.route('DELETE','/installations/:objectId', (req) => { return this.handleDelete(req); }); + let router = new PromiseRouter(); + router.route('GET','/installations', req => { return this.handleFind(req); }); + router.route('GET','/installations/:objectId', req => { return this.handleGet(req); }); + router.route('POST','/installations', req => { return this.handleCreate(req); }); + router.route('PUT','/installations/:objectId', req => { return this.handleUpdate(req); }); + router.route('DELETE','/installations/:objectId', req => { return this.handleDelete(req); }); return router; } } From f53cb60d57f1eddedd3c4846b991ec5b8aa9ae62 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Thu, 11 Feb 2016 21:53:32 -0800 Subject: [PATCH 5/8] Add enforceMasterKeyAccess middleware. --- spec/ParseFile.spec.js | 8 ++++---- src/Controllers/FilesController.js | 8 +------- src/middlewares.js | 13 +++++++++++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 7287dd14..8613f3a2 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -101,8 +101,8 @@ describe('Parse.File testing', () => { }, (error, response, body) => { expect(error).toBe(null); var del_b = JSON.parse(body); - expect(response.statusCode).toEqual(400); - expect(del_b.code).toEqual(119); + expect(response.statusCode).toEqual(403); + expect(del_b.error).toMatch(/unauthorized/); // incorrect X-Parse-Master-Key header request.del({ headers: { @@ -114,8 +114,8 @@ describe('Parse.File testing', () => { }, (error, response, body) => { expect(error).toBe(null); var del_b2 = JSON.parse(body); - expect(response.statusCode).toEqual(400); - expect(del_b2.code).toEqual(119); + expect(response.statusCode).toEqual(403); + expect(del_b2.error).toMatch(/unauthorized/); done(); }); }); diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 321042b9..dac6b684 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -76,13 +76,6 @@ export class FilesController { deleteHandler() { return (req, res, next) => { - // enforce use of master key for file deletions - if(!req.auth.isMaster){ - next(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, - 'Master key required for file deletion.')); - return; - } - this._filesAdapter.deleteFile(req.config, req.params.filename).then(() => { res.status(200); // TODO: return useful JSON here? @@ -142,6 +135,7 @@ export class FilesController { router.delete('/files/:filename', Middlewares.allowCrossDomain, Middlewares.handleParseHeaders, + Middlewares.enforceMasterKeyAccess, this.deleteHandler() ); diff --git a/src/middlewares.js b/src/middlewares.js index bb251239..a07b2a1b 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -178,15 +178,24 @@ var handleParseErrors = function(err, req, res, next) { } }; +function enforceMasterKeyAccess(req, res, next) { + if (!req.auth.isMaster) { + res.status(403); + res.end('{"error":"unauthorized: master key is required"}'); + return; + } + next(); +} + function invalidRequest(req, res) { res.status(403); res.end('{"error":"unauthorized"}'); } - module.exports = { allowCrossDomain: allowCrossDomain, allowMethodOverride: allowMethodOverride, handleParseErrors: handleParseErrors, - handleParseHeaders: handleParseHeaders + handleParseHeaders: handleParseHeaders, + enforceMasterKeyAccess: enforceMasterKeyAccess }; From 62e671dd9e6e495da79f4d7051935960d2e442d0 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Thu, 11 Feb 2016 21:35:56 -0800 Subject: [PATCH 6/8] Refactor and deduplicate RolesRouter, fix missing query on /roles. --- src/Routers/RolesRouter.js | 43 ++++++++++++++++++++++++++++++++++ src/index.js | 3 ++- src/roles.js | 48 -------------------------------------- 3 files changed, 45 insertions(+), 49 deletions(-) create mode 100644 src/Routers/RolesRouter.js delete mode 100644 src/roles.js diff --git a/src/Routers/RolesRouter.js b/src/Routers/RolesRouter.js new file mode 100644 index 00000000..b20a91ee --- /dev/null +++ b/src/Routers/RolesRouter.js @@ -0,0 +1,43 @@ + +import ClassesRouter from './ClassesRouter'; +import PromiseRouter from '../PromiseRouter'; +import rest from '../rest'; + +export class RolesRouter extends ClassesRouter { + handleFind(req) { + req.params.className = '_Role'; + return super.handleFind(req); + } + + handleGet(req) { + req.params.className = '_Role'; + return super.handleGet(req); + } + + handleCreate(req) { + req.params.className = '_Role'; + return super.handleCreate(req); + } + + handleUpdate(req) { + req.params.className = '_Role'; + return super.handleUpdate(req); + } + + handleDelete(req) { + req.params.className = '_Role'; + return super.handleDelete(req); + } + + getExpressRouter() { + let router = new PromiseRouter(); + router.route('GET','/roles', req => { return this.handleFind(req); }); + router.route('GET','/roles/:objectId', req => { return this.handleGet(req); }); + router.route('POST','/roles', req => { return this.handleCreate(req); }); + router.route('PUT','/roles/:objectId', req => { return this.handleUpdate(req); }); + router.route('DELETE','/roles/:objectId', req => { return this.handleDelete(req); }); + return router; + } +} + +export default RolesRouter; diff --git a/src/index.js b/src/index.js index 9d9910bd..952c1c05 100644 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,7 @@ import { ClassesRouter } from './Routers/ClassesRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; import { UsersRouter } from './Routers/UsersRouter'; import { SessionsRouter } from './Routers/SessionsRouter'; +import { RolesRouter } from './Routers/RolesRouter'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -133,7 +134,7 @@ function ParseServer(args) { new ClassesRouter().getExpressRouter(), new UsersRouter().getExpressRouter(), new SessionsRouter().getExpressRouter(), - require('./roles'), + new RolesRouter().getExpressRouter(), require('./analytics'), new InstallationsRouter().getExpressRouter(), require('./functions'), diff --git a/src/roles.js b/src/roles.js deleted file mode 100644 index 6aaf8065..00000000 --- a/src/roles.js +++ /dev/null @@ -1,48 +0,0 @@ -// roles.js - -var Parse = require('parse/node').Parse, - PromiseRouter = require('./PromiseRouter'), - rest = require('./rest'); - -var router = new PromiseRouter(); - -function handleCreate(req) { - return rest.create(req.config, req.auth, - '_Role', req.body); -} - -function handleUpdate(req) { - return rest.update(req.config, req.auth, '_Role', - req.params.objectId, req.body) - .then((response) => { - return {response: response}; - }); -} - -function handleDelete(req) { - return rest.del(req.config, req.auth, - '_Role', req.params.objectId) - .then(() => { - return {response: {}}; - }); -} - -function handleGet(req) { - return rest.find(req.config, req.auth, '_Role', - {objectId: req.params.objectId}) - .then((response) => { - if (!response.results || response.results.length == 0) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.'); - } else { - return {response: response.results[0]}; - } - }); -} - -router.route('POST','/roles', handleCreate); -router.route('GET','/roles/:objectId', handleGet); -router.route('PUT','/roles/:objectId', handleUpdate); -router.route('DELETE','/roles/:objectId', handleDelete); - -module.exports = router; \ No newline at end of file From dc4859f561dfae078d4529d8cfa4405ac4daf4c9 Mon Sep 17 00:00:00 2001 From: Peter Shin Date: Thu, 4 Feb 2016 08:18:19 -0800 Subject: [PATCH 7/8] Logs support. Added /logs endpoint with basic logger and LoggerAdapter. --- package.json | 3 +- spec/FileLoggerAdapter.spec.js | 64 +++++++ spec/LoggerController.spec.js | 55 ++++++ src/Adapters/Logger/FileLoggerAdapter.js | 225 +++++++++++++++++++++++ src/Adapters/Logger/LoggerAdapter.js | 17 ++ src/Controllers/LoggerController.js | 78 ++++++++ src/index.js | 9 +- 7 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 spec/FileLoggerAdapter.spec.js create mode 100644 spec/LoggerController.spec.js create mode 100644 src/Adapters/Logger/FileLoggerAdapter.js create mode 100644 src/Adapters/Logger/LoggerAdapter.js create mode 100644 src/Controllers/LoggerController.js diff --git a/package.json b/package.json index 521381f0..97467963 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "node-gcm": "^0.14.0", "parse": "^1.7.0", "randomstring": "^1.1.3", - "request": "^2.65.0" + "request": "^2.65.0", + "winston": "^2.1.1" }, "devDependencies": { "babel-cli": "^6.5.1", diff --git a/spec/FileLoggerAdapter.spec.js b/spec/FileLoggerAdapter.spec.js new file mode 100644 index 00000000..4466e087 --- /dev/null +++ b/spec/FileLoggerAdapter.spec.js @@ -0,0 +1,64 @@ +var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter; +var Parse = require('parse/node').Parse; +var request = require('request'); +var fs = require('fs'); + +var LOGS_FOLDER = './test_logs/'; + +var deleteFolderRecursive = function(path) { + if( fs.existsSync(path) ) { + fs.readdirSync(path).forEach(function(file,index){ + var curPath = path + "/" + file; + if(fs.lstatSync(curPath).isDirectory()) { // recurse + deleteFolderRecursive(curPath); + } else { // delete file + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(path); + } +}; + +describe('info logs', () => { + + afterEach((done) => { + deleteFolderRecursive(LOGS_FOLDER); + done(); + }); + + it("Verify INFO logs", (done) => { + var fileLoggerAdapter = new FileLoggerAdapter({ + logsFolder: LOGS_FOLDER + }); + fileLoggerAdapter.info('testing info logs', () => { + fileLoggerAdapter.query({ + size: 1, + level: 'info' + }, (results) => { + expect(results[0].message).toEqual('testing info logs'); + done(); + }); + }); + }); +}); + +describe('error logs', () => { + + afterEach((done) => { + deleteFolderRecursive(LOGS_FOLDER); + done(); + }); + + it("Verify ERROR logs", (done) => { + var fileLoggerAdapter = new FileLoggerAdapter(); + fileLoggerAdapter.error('testing error logs', () => { + fileLoggerAdapter.query({ + size: 1, + level: 'error' + }, (results) => { + expect(results[0].message).toEqual('testing error logs'); + done(); + }); + }); + }); +}); diff --git a/spec/LoggerController.spec.js b/spec/LoggerController.spec.js new file mode 100644 index 00000000..f23004ab --- /dev/null +++ b/spec/LoggerController.spec.js @@ -0,0 +1,55 @@ +var LoggerController = require('../src/Controllers/LoggerController').LoggerController; +var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter; + +describe('LoggerController', () => { + it('can check valid master key of request', (done) => { + // Make mock request + var request = { + auth: { + isMaster: true + }, + query: {} + }; + + var loggerController = new LoggerController(new FileLoggerAdapter()); + + expect(() => { + loggerController.handleGET(request); + }).not.toThrow(); + done(); + }); + + it('can check invalid construction of controller', (done) => { + // Make mock request + var request = { + auth: { + isMaster: true + }, + query: {} + }; + + var loggerController = new LoggerController(); + + expect(() => { + loggerController.handleGET(request); + }).toThrow(); + done(); + }); + + it('can check invalid master key of request', (done) => { + // Make mock request + var request = { + auth: { + isMaster: false + }, + query: {} + }; + + var loggerController = new LoggerController(new FileLoggerAdapter()); + + expect(() => { + loggerController.handleGET(request); + }).toThrow(); + done(); + }); +}); diff --git a/src/Adapters/Logger/FileLoggerAdapter.js b/src/Adapters/Logger/FileLoggerAdapter.js new file mode 100644 index 00000000..4edc4122 --- /dev/null +++ b/src/Adapters/Logger/FileLoggerAdapter.js @@ -0,0 +1,225 @@ +// Logger +// +// Wrapper around Winston logging library with custom query +// +// expected log entry to be in the shape of: +// {"level":"info","message":"{ '0': 'Your Message' }","timestamp":"2016-02-04T05:59:27.412Z"} +// +import { LoggerAdapter } from './LoggerAdapter'; +import winston from 'winston'; +import fs from 'fs'; +import { Parse } from 'parse/node'; + +const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; +const CACHE_TIME = 1000 * 60; + +let LOGS_FOLDER = './logs/'; + +if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { + LOGS_FOLDER = './test_logs/' +} + +let currentDate = new Date(); + +let simpleCache = { + timestamp: null, + from: null, + until: null, + order: null, + data: [], + level: 'info', +}; + +// returns Date object rounded to nearest day +let _getNearestDay = (date) => { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} + +// returns Date object of previous day +let _getPrevDay = (date) => { + return new Date(date - MILLISECONDS_IN_A_DAY); +} + +// returns the iso formatted file name +let _getFileName = () => { + return _getNearestDay(currentDate).toISOString() +} + +// check for valid cache when both from and util match. +// cache valid for up to 1 minute +let _hasValidCache = (from, until, level) => { + if (String(from) === String(simpleCache.from) && + String(until) === String(simpleCache.until) && + new Date() - simpleCache.timestamp < CACHE_TIME && + level === simpleCache.level) { + return true; + } + return false; +} + +// renews transports to current date +let _renewTransports = ({infoLogger, errorLogger, logsFolder}) => { + if (infoLogger) { + infoLogger.add(winston.transports.File, { + filename: logsFolder + _getFileName() + '.info', + name: 'info-file', + level: 'info' + }); + } + if (errorLogger) { + errorLogger.add(winston.transports.File, { + filename: logsFolder + _getFileName() + '.error', + name: 'error-file', + level: 'error' + }); + } +}; + +// check that log entry has valid time stamp based on query +let _isValidLogEntry = (from, until, entry) => { + var _entry = JSON.parse(entry), + timestamp = new Date(_entry.timestamp); + return timestamp >= from && timestamp <= until + ? true + : false +}; + +// ensure that file name is up to date +let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => { + if (_getNearestDay(currentDate) !== _getNearestDay(new Date())) { + currentDate = new Date(); + if (infoLogger) { + infoLogger.remove('info-file'); + } + if (errorLogger) { + errorLogger.remove('error-file'); + } + _renewTransports({infoLogger, errorLogger, logsFolder}); + } +} + +export class FileLoggerAdapter extends LoggerAdapter { + constructor(options = {}) { + super(); + + this._logsFolder = options.logsFolder || LOGS_FOLDER; + + // check logs folder exists + if (!fs.existsSync(this._logsFolder)) { + fs.mkdirSync(this._logsFolder); + } + + this._errorLogger = new (winston.Logger)({ + exitOnError: false, + transports: [ + new (winston.transports.File)({ + filename: this._logsFolder + _getFileName() + '.error', + name: 'error-file', + level: 'error' + }) + ] + }); + + this._infoLogger = new (winston.Logger)({ + exitOnError: false, + transports: [ + new (winston.transports.File)({ + filename: this._logsFolder + _getFileName() + '.info', + name: 'info-file', + level: 'info' + }) + ] + }); + } + + info() { + _verifyTransports({infoLogger: this._infoLogger, logsFolder: this._logsFolder}); + return this._infoLogger.info.apply(undefined, arguments); + } + + error() { + _verifyTransports({errorLogger: this._errorLogger, logsFolder: this._logsFolder}); + return this._errorLogger.error.apply(undefined, arguments); + } + + // custom query as winston is currently limited + query(options, callback) { + if (!options) { + options = {}; + } + // defaults to 7 days prior + let from = options.from || new Date(Date.now() - (7 * MILLISECONDS_IN_A_DAY)); + let until = options.until || new Date(); + let size = options.size || 10; + let order = options.order || 'desc'; + let level = options.level || 'info'; + let roundedUntil = _getNearestDay(until); + let roundedFrom = _getNearestDay(from); + + if (_hasValidCache(roundedFrom, roundedUntil, level)) { + let logs = []; + if (order !== simpleCache.order) { + // reverse order of data + simpleCache.data.forEach((entry) => { + logs.unshift(entry); + }); + } else { + logs = simpleCache.data; + } + callback(logs.slice(0, size)); + return; + } + + let curDate = roundedUntil; + let curSize = 0; + let method = order === 'desc' ? 'push' : 'unshift'; + let files = []; + let promises = []; + + // current a batch call, all files with valid dates are read + while (curDate >= from) { + files[method](this._logsFolder + curDate.toISOString() + '.' + level); + curDate = _getPrevDay(curDate); + } + + // read each file and split based on newline char. + // limitation is message cannot contain newline + // TODO: strip out delimiter from logged message + files.forEach(function(file, i) { + let promise = new Parse.Promise(); + fs.readFile(file, 'utf8', function(err, data) { + if (err) { + promise.resolve([]); + } else { + let results = data.split('\n').filter((value) => { + return value.trim() !== ''; + }); + promise.resolve(results); + } + }); + promises[method](promise); + }); + + Parse.Promise.when(promises).then((results) => { + let logs = []; + results.forEach(function(logEntries, i) { + logEntries.forEach(function(entry) { + if (_isValidLogEntry(from, until, entry)) { + logs[method](JSON.parse(entry)); + } + }); + }); + simpleCache = { + timestamp: new Date(), + from: roundedFrom, + until: roundedUntil, + data: logs, + order, + level, + }; + callback(logs.slice(0, size)); + }); + } +} + +export default FileLoggerAdapter; diff --git a/src/Adapters/Logger/LoggerAdapter.js b/src/Adapters/Logger/LoggerAdapter.js new file mode 100644 index 00000000..b1fe31b8 --- /dev/null +++ b/src/Adapters/Logger/LoggerAdapter.js @@ -0,0 +1,17 @@ +// Logger Adapter +// +// Allows you to change the logger mechanism +// +// Adapter classes must implement the following functions: +// * info(obj1 [, obj2, .., objN]) +// * error(obj1 [, obj2, .., objN]) +// * query(options, callback) +// Default is FileLoggerAdapter.js + +export class LoggerAdapter { + info() {} + error() {} + query(options, callback) {} +} + +export default LoggerAdapter; diff --git a/src/Controllers/LoggerController.js b/src/Controllers/LoggerController.js new file mode 100644 index 00000000..d0b8bb28 --- /dev/null +++ b/src/Controllers/LoggerController.js @@ -0,0 +1,78 @@ +import { Parse } from 'parse/node'; +import PromiseRouter from '../PromiseRouter'; +import rest from '../rest'; + +const Promise = Parse.Promise; +const INFO = 'info'; +const ERROR = 'error'; +const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; + +// only allow request with master key +let enforceSecurity = (auth) => { + if (!auth || !auth.isMaster) { + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + 'Clients aren\'t allowed to perform the ' + + 'get' + ' operation on logs.' + ); + } +} + +// check that date input is valid +let isValidDateTime = (date) => { + if (!date || isNaN(Number(date))) { + return false; + } +} + +export class LoggerController { + + constructor(loggerAdapter) { + this._loggerAdapter = loggerAdapter; + } + + // Returns a promise for a {response} object. + // query params: + // level (optional) Level of logging you want to query for (info || error) + // from (optional) Start time for the search. Defaults to 1 week ago. + // until (optional) End time for the search. Defaults to current time. + // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”. + // size (optional) Number of rows returned by search. Defaults to 10 + handleGET(req) { + if (!this._loggerAdapter) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Logger adapter is not availabe'); + } + + let promise = new Parse.Promise(); + let from = (isValidDateTime(req.query.from) && new Date(req.query.from)) || + new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); + let until = (isValidDateTime(req.query.until) && new Date(req.query.until)) || new Date(); + let size = Number(req.query.size) || 10; + let order = req.query.order || 'desc'; + let level = req.query.level || INFO; + enforceSecurity(req.auth); + this._loggerAdapter.query({ + from, + until, + size, + order, + level, + }, (result) => { + promise.resolve({ + response: result + }); + }); + return promise; + } + + getExpressRouter() { + let router = new PromiseRouter(); + router.route('GET','/logs', (req) => { + return this.handleGET(req); + }); + return router; + } +} + +export default LoggerController; diff --git a/src/index.js b/src/index.js index c2993400..4458f8c0 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,9 @@ import { PushController } from './Controllers/PushController'; import { ClassesRouter } from './Routers/ClassesRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; +import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; +import { LoggerController } from './Controllers/LoggerController'; + // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -69,6 +72,9 @@ function ParseServer(args) { pushAdapter = new ParsePushAdapter(pushConfig) } + // Make logger adapter + let loggerAdapter = args.loggerAdapter || new FileLoggerAdapter(); + if (args.databaseURI) { DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI); } @@ -136,7 +142,8 @@ function ParseServer(args) { new InstallationsRouter().getExpressRouter(), require('./functions'), require('./schemas'), - new PushController(pushAdapter).getExpressRouter() + new PushController(pushAdapter).getExpressRouter(), + new LoggerController(loggerAdapter).getExpressRouter() ]; if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { routers.push(require('./global_config')); From 0c75f6022d32ead36c1ae44eb2dab7ae9ab74485 Mon Sep 17 00:00:00 2001 From: "Peter J. Shin" Date: Fri, 12 Feb 2016 10:30:04 -0800 Subject: [PATCH 8/8] Updating adapter docs. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dfa8b0f0..92b7668f 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ The client keys used with Parse are no longer necessary with parse-server. If y #### Advanced options: -* filesAdapter - The default behavior (GridStore) can be changed by creating an adapter class (see `FilesAdapter.js`) +* 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)) * databaseAdapter (unfinished) - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`) +* loggerAdapter - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)) ---