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)) --- diff --git a/package.json b/package.json index 521381f0..ae2b5331 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,13 @@ "body-parser": "^1.14.2", "deepcopy": "^0.6.1", "express": "^4.13.4", - "hat": "~0.0.3", "mime": "^1.3.4", "mongodb": "~2.1.0", "multer": "^1.1.0", "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/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index b65d8f34..8613f3a2 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(403); + expect(del_b.error).toMatch(/unauthorized/); + // incorrect X-Parse-Master-Key header + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'tryagain' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + var del_b2 = JSON.parse(body); + expect(response.statusCode).toEqual(403); + expect(del_b2.error).toMatch(/unauthorized/); + done(); + }); + }); + }); + }); + it('handles other filetypes', done => { var headers = { 'Content-Type': 'image/jpeg', diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 368bea22..787a8ecb 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1358,6 +1358,25 @@ describe('Parse.User testing', () => { }); }); + it('retrieve user data from fetch, make sure the session token hasn\'t changed', (done) => { + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + var currentSessionToken = ""; + Parse.Promise.as().then(function() { + return user.signUp(); + }).then(function(){ + currentSessionToken = user.getSessionToken(); + return user.fetch(); + }).then(function(u){ + expect(currentSessionToken).toEqual(u.getSessionToken()); + done(); + }, function(error) { + ok(false, error); + done(); + }) + }); + it('user save should fail with invalid email', (done) => { var user = new Parse.User(); user.set('username', 'teste'); diff --git a/spec/cryptoUtils.spec.js b/spec/cryptoUtils.spec.js new file mode 100644 index 00000000..cd996770 --- /dev/null +++ b/spec/cryptoUtils.spec.js @@ -0,0 +1,83 @@ +var cryptoUtils = require('../src/cryptoUtils'); + +function givesUniqueResults(fn, iterations) { + var results = {}; + for (var i = 0; i < iterations; i++) { + var s = fn(); + if (results[s]) { + return false; + } + results[s] = true; + } + return true; +} + +describe('randomString', () => { + it('returns a string', () => { + expect(typeof cryptoUtils.randomString(10)).toBe('string'); + }); + + it('returns result of the given length', () => { + expect(cryptoUtils.randomString(11).length).toBe(11); + expect(cryptoUtils.randomString(25).length).toBe(25); + }); + + it('throws if requested length is zero', () => { + expect(() => cryptoUtils.randomString(0)).toThrow(); + }); + + it('returns unique results', () => { + expect(givesUniqueResults(() => cryptoUtils.randomString(10), 100)).toBe(true); + }); +}); + +describe('randomHexString', () => { + it('returns a string', () => { + expect(typeof cryptoUtils.randomHexString(10)).toBe('string'); + }); + + it('returns result of the given length', () => { + expect(cryptoUtils.randomHexString(10).length).toBe(10); + expect(cryptoUtils.randomHexString(32).length).toBe(32); + }); + + it('throws if requested length is zero', () => { + expect(() => cryptoUtils.randomHexString(0)).toThrow(); + }); + + it('throws if requested length is not even', () => { + expect(() => cryptoUtils.randomHexString(11)).toThrow(); + }); + + it('returns unique results', () => { + expect(givesUniqueResults(() => cryptoUtils.randomHexString(20), 100)).toBe(true); + }); +}); + +describe('newObjectId', () => { + it('returns a string', () => { + expect(typeof cryptoUtils.newObjectId()).toBe('string'); + }); + + it('returns result with at least 10 characters', () => { + expect(cryptoUtils.newObjectId().length).toBeGreaterThan(9); + }); + + it('returns unique results', () => { + expect(givesUniqueResults(() => cryptoUtils.newObjectId(), 100)).toBe(true); + }); +}); + +describe('newToken', () => { + it('returns a string', () => { + expect(typeof cryptoUtils.newToken()).toBe('string'); + }); + + it('returns result with at least 32 characters', () => { + expect(cryptoUtils.newToken().length).toBeGreaterThan(31); + }); + + it('returns unique results', () => { + expect(givesUniqueResults(() => cryptoUtils.newToken(), 100)).toBe(true); + }); +}); 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/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/FilesController.js b/src/Controllers/FilesController.js index 47454f07..6fde54b7 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -4,11 +4,9 @@ import express from 'express'; import mime from 'mime'; import { Parse } from 'parse/node'; import BodyParser from 'body-parser'; -import hat from 'hat'; import * as Middlewares from '../middlewares'; import Config from '../Config'; - -const rack = hat.rack(); +import { randomHexString } from '../cryptoUtils'; export class FilesController { constructor(filesAdapter) { @@ -61,7 +59,7 @@ export class FilesController { extension = '.' + mime.extension(contentType); } - let filename = rack() + '_' + req.params.filename + extension; + let filename = randomHexString(32) + '_' + req.params.filename + extension; this._filesAdapter.createFile(req.config, filename, req.body).then(() => { res.status(201); var location = this._filesAdapter.getFileLocation(req.config, filename); @@ -74,6 +72,19 @@ export class FilesController { }; } + deleteHandler() { + return (req, res, next) => { + 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 +130,13 @@ export class FilesController { this.createHandler() ); + router.delete('/files/:filename', + Middlewares.allowCrossDomain, + Middlewares.handleParseHeaders, + Middlewares.enforceMasterKeyAccess, + this.deleteHandler() + ); + return router; } } 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/GCM.js b/src/GCM.js index be09f222..a13a6751 100644 --- a/src/GCM.js +++ b/src/GCM.js @@ -2,7 +2,7 @@ const Parse = require('parse/node').Parse; const gcm = require('node-gcm'); -const randomstring = require('randomstring'); +const cryptoUtils = require('./cryptoUtils'); const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks const GCMRegistrationTokensMax = 1000; @@ -22,10 +22,7 @@ function GCM(args) { * @returns {Object} A promise which is resolved after we get results from gcm */ GCM.prototype.send = function(data, devices) { - let pushId = randomstring.generate({ - length: 10, - charset: 'alphanumeric' - }); + let pushId = cryptoUtils.newObjectId(); let timeStamp = Date.now(); let expirationTime; // We handle the expiration_time convertion in push.js, so expiration_time is a valid date diff --git a/src/RestQuery.js b/src/RestQuery.js index 91ebe536..7cf8074f 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -415,6 +415,11 @@ function includePath(config, auth, response, path) { for (var obj of includeResponse.results) { obj.__type = 'Object'; obj.className = className; + + if(className == "_User"){ + delete obj.sessionToken; + } + replace[obj.objectId] = obj; } var resp = { diff --git a/src/RestWrite.js b/src/RestWrite.js index 3832d59f..497ac5b1 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -2,13 +2,12 @@ // 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(); var Auth = require('./Auth'); var cache = require('./cache'); var Config = require('./Config'); +var cryptoUtils = require('./cryptoUtils'); var passwordCrypto = require('./password'); var facebook = require('./facebook'); var Parse = require('parse/node'); @@ -57,7 +56,7 @@ function RestWrite(config, auth, className, query, data, originalData) { this.data.updatedAt = this.updatedAt; if (!this.query) { this.data.createdAt = this.updatedAt; - this.data.objectId = newStringId(10); + this.data.objectId = cryptoUtils.newObjectId(); } } } @@ -268,7 +267,7 @@ RestWrite.prototype.handleFacebookAuthData = function() { throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used'); } else { - this.data.username = rack(); + this.data.username = cryptoUtils.newToken(); } // This FB auth does not already exist, so transform it to a @@ -289,7 +288,7 @@ RestWrite.prototype.transformUser = function() { var promise = Promise.resolve(); if (!this.query) { - var token = 'r:' + rack(); + var token = 'r:' + cryptoUtils.newToken(); this.storage['token'] = token; promise = promise.then(() => { var expiresAt = new Date(); @@ -335,7 +334,7 @@ RestWrite.prototype.transformUser = function() { // Check for username uniqueness if (!this.data.username) { if (!this.query) { - this.data.username = newStringId(25); + this.data.username = cryptoUtils.randomString(25); } return; } @@ -428,7 +427,7 @@ RestWrite.prototype.handleSession = function() { } if (!this.query && !this.auth.isMaster) { - var token = 'r:' + rack(); + var token = 'r:' + cryptoUtils.newToken(); var expiresAt = new Date(); expiresAt.setFullYear(expiresAt.getFullYear() + 1); var sessionData = { @@ -721,20 +720,4 @@ RestWrite.prototype.objectId = function() { return this.data.objectId || this.query.objectId; }; -// Returns a unique string that's usable as an object or other id. -function newStringId(size) { - var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + - 'abcdefghijklmnopqrstuvwxyz' + - '0123456789'); - var objectId = ''; - var bytes = crypto.randomBytes(size); - 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; -} - module.exports = RestWrite; diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 11666b20..a49d6d4a 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -51,6 +51,11 @@ export class ClassesRouter { if (!response.results || response.results.length == 0) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); } + + if(req.params.className === "_User"){ + delete response.results[0].sessionToken; + } + return { response: response.results[0] }; }); } 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; } } 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/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/Routers/UsersRouter.js b/src/Routers/UsersRouter.js new file mode 100644 index 00000000..5b894f75 --- /dev/null +++ b/src/Routers/UsersRouter.js @@ -0,0 +1,161 @@ +// These methods handle the User-related routes. + +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'; +import { newToken } from '../cryptoUtils'; + +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:' + newToken(); + 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/cryptoUtils.js b/src/cryptoUtils.js new file mode 100644 index 00000000..2f83defd --- /dev/null +++ b/src/cryptoUtils.js @@ -0,0 +1,44 @@ +import { randomBytes } from 'crypto'; + +// Returns a new random hex string of the given even size. +export function randomHexString(size) { + if (size === 0) { + throw new Error('Zero-length randomHexString is useless.'); + } + if (size % 2 !== 0) { + throw new Error('randomHexString size must be divisible by 2.') + } + return randomBytes(size/2).toString('hex'); +} + +// Returns a new random alphanumeric string of the given size. +// +// Note: to simplify implementation, the result has slight modulo bias, +// because chars length of 62 doesn't divide the number of all bytes +// (256) evenly. Such bias is acceptable for most cases when the output +// length is long enough and doesn't need to be uniform. +export function randomString(size) { + if (size === 0) { + throw new Error('Zero-length randomString is useless.'); + } + var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + + 'abcdefghijklmnopqrstuvwxyz' + + '0123456789'); + var objectId = ''; + var bytes = randomBytes(size); + for (var i = 0; i < bytes.length; ++i) { + objectId += chars[bytes.readUInt8(i) % chars.length]; + } + return objectId; +} + +// Returns a new random alphanumeric string suitable for object ID. +export function newObjectId() { + //TODO: increase length to better protect against collisions. + return randomString(10); +} + +// Returns a new random hex string suitable for secure tokens. +export function newToken() { + return randomHexString(32); +} diff --git a/src/functions.js b/src/functions.js index 09e43ed3..f8b8fbc9 100644 --- a/src/functions.js +++ b/src/functions.js @@ -22,6 +22,7 @@ function handleCloudFunction(req) { params: req.body || {}, master: req.auth && req.auth.isMaster, user: req.auth && req.auth.user, + installationId: req.info.installationId }; Parse.Cloud.Functions[req.params.functionName](request, response); }); diff --git a/src/index.js b/src/index.js index c2993400..fef09075 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,12 @@ import { PushController } from './Controllers/PushController'; import { ClassesRouter } from './Routers/ClassesRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; +import { UsersRouter } from './Routers/UsersRouter'; +import { SessionsRouter } from './Routers/SessionsRouter'; +import { RolesRouter } from './Routers/RolesRouter'; + +import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; +import { LoggerController } from './Controllers/LoggerController'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -69,6 +75,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); } @@ -129,14 +138,15 @@ function ParseServer(args) { let routers = [ new ClassesRouter().getExpressRouter(), - require('./users'), - require('./sessions'), - require('./roles'), + new UsersRouter().getExpressRouter(), + new SessionsRouter().getExpressRouter(), + new RolesRouter().getExpressRouter(), require('./analytics'), 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')); 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 }; 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 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; diff --git a/src/testing-routes.js b/src/testing-routes.js index 85db1485..28b02cf4 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -3,13 +3,13 @@ var express = require('express'), cache = require('./cache'), middlewares = require('./middlewares'), - rack = require('hat').rack(); + cryptoUtils = require('./cryptoUtils'); var router = express.Router(); // creates a unique app in the cache, with a collection prefix function createApp(req, res) { - var appId = rack(); + var appId = cryptoUtils.randomHexString(32); cache.apps[appId] = { 'collectionPrefix': appId + '_', 'masterKey': 'master' @@ -70,4 +70,4 @@ router.post('/rest_configure_app', module.exports = { router: router -}; \ No newline at end of file +}; 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;