Merge remote-tracking branch 'ParsePlatform/master' into user-roles
This commit is contained in:
@@ -38,8 +38,9 @@ The client keys used with Parse are no longer necessary with parse-server. If y
|
|||||||
|
|
||||||
#### Advanced options:
|
#### 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`)
|
* 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))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,13 @@
|
|||||||
"body-parser": "^1.14.2",
|
"body-parser": "^1.14.2",
|
||||||
"deepcopy": "^0.6.1",
|
"deepcopy": "^0.6.1",
|
||||||
"express": "^4.13.4",
|
"express": "^4.13.4",
|
||||||
"hat": "~0.0.3",
|
|
||||||
"mime": "^1.3.4",
|
"mime": "^1.3.4",
|
||||||
"mongodb": "~2.1.0",
|
"mongodb": "~2.1.0",
|
||||||
"multer": "^1.1.0",
|
"multer": "^1.1.0",
|
||||||
"node-gcm": "^0.14.0",
|
"node-gcm": "^0.14.0",
|
||||||
"parse": "^1.7.0",
|
"parse": "^1.7.0",
|
||||||
"randomstring": "^1.1.3",
|
"request": "^2.65.0",
|
||||||
"request": "^2.65.0"
|
"winston": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.5.1",
|
"babel-cli": "^6.5.1",
|
||||||
|
|||||||
64
spec/FileLoggerAdapter.spec.js
Normal file
64
spec/FileLoggerAdapter.spec.js
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
55
spec/LoggerController.spec.js
Normal file
55
spec/LoggerController.spec.js
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 => {
|
it('handles other filetypes', done => {
|
||||||
var headers = {
|
var headers = {
|
||||||
'Content-Type': 'image/jpeg',
|
'Content-Type': 'image/jpeg',
|
||||||
|
|||||||
@@ -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) => {
|
it('user save should fail with invalid email', (done) => {
|
||||||
var user = new Parse.User();
|
var user = new Parse.User();
|
||||||
user.set('username', 'teste');
|
user.set('username', 'teste');
|
||||||
|
|||||||
83
spec/cryptoUtils.spec.js
Normal file
83
spec/cryptoUtils.spec.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,8 @@
|
|||||||
export class FilesAdapter {
|
export class FilesAdapter {
|
||||||
createFile(config, filename, data) { }
|
createFile(config, filename, data) { }
|
||||||
|
|
||||||
|
deleteFile(config, filename) { }
|
||||||
|
|
||||||
getFileData(config, filename) { }
|
getFileData(config, filename) { }
|
||||||
|
|
||||||
getFileLocation(config, filename) { }
|
getFileLocation(config, filename) { }
|
||||||
|
|||||||
@@ -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) {
|
getFileData(config, filename) {
|
||||||
return config.database.connect().then(() => {
|
return config.database.connect().then(() => {
|
||||||
return GridStore.exist(config.database.db, filename);
|
return GridStore.exist(config.database.db, filename);
|
||||||
|
|||||||
@@ -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
|
// Search for and return a file if found by filename
|
||||||
// Returns a promise that succeeds with the buffer result from S3
|
// Returns a promise that succeeds with the buffer result from S3
|
||||||
getFileData(config, filename) {
|
getFileData(config, filename) {
|
||||||
|
|||||||
225
src/Adapters/Logger/FileLoggerAdapter.js
Normal file
225
src/Adapters/Logger/FileLoggerAdapter.js
Normal file
@@ -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;
|
||||||
17
src/Adapters/Logger/LoggerAdapter.js
Normal file
17
src/Adapters/Logger/LoggerAdapter.js
Normal file
@@ -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;
|
||||||
@@ -4,11 +4,9 @@ import express from 'express';
|
|||||||
import mime from 'mime';
|
import mime from 'mime';
|
||||||
import { Parse } from 'parse/node';
|
import { Parse } from 'parse/node';
|
||||||
import BodyParser from 'body-parser';
|
import BodyParser from 'body-parser';
|
||||||
import hat from 'hat';
|
|
||||||
import * as Middlewares from '../middlewares';
|
import * as Middlewares from '../middlewares';
|
||||||
import Config from '../Config';
|
import Config from '../Config';
|
||||||
|
import { randomHexString } from '../cryptoUtils';
|
||||||
const rack = hat.rack();
|
|
||||||
|
|
||||||
export class FilesController {
|
export class FilesController {
|
||||||
constructor(filesAdapter) {
|
constructor(filesAdapter) {
|
||||||
@@ -61,7 +59,7 @@ export class FilesController {
|
|||||||
extension = '.' + mime.extension(contentType);
|
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(() => {
|
this._filesAdapter.createFile(req.config, filename, req.body).then(() => {
|
||||||
res.status(201);
|
res.status(201);
|
||||||
var location = this._filesAdapter.getFileLocation(req.config, filename);
|
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
|
* Find file references in REST-format object and adds the url key
|
||||||
* with the current mount point and app id.
|
* with the current mount point and app id.
|
||||||
@@ -119,6 +130,13 @@ export class FilesController {
|
|||||||
this.createHandler()
|
this.createHandler()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.delete('/files/:filename',
|
||||||
|
Middlewares.allowCrossDomain,
|
||||||
|
Middlewares.handleParseHeaders,
|
||||||
|
Middlewares.enforceMasterKeyAccess,
|
||||||
|
this.deleteHandler()
|
||||||
|
);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/Controllers/LoggerController.js
Normal file
78
src/Controllers/LoggerController.js
Normal file
@@ -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;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const Parse = require('parse/node').Parse;
|
const Parse = require('parse/node').Parse;
|
||||||
const gcm = require('node-gcm');
|
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 GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
|
||||||
const GCMRegistrationTokensMax = 1000;
|
const GCMRegistrationTokensMax = 1000;
|
||||||
@@ -22,10 +22,7 @@ function GCM(args) {
|
|||||||
* @returns {Object} A promise which is resolved after we get results from gcm
|
* @returns {Object} A promise which is resolved after we get results from gcm
|
||||||
*/
|
*/
|
||||||
GCM.prototype.send = function(data, devices) {
|
GCM.prototype.send = function(data, devices) {
|
||||||
let pushId = randomstring.generate({
|
let pushId = cryptoUtils.newObjectId();
|
||||||
length: 10,
|
|
||||||
charset: 'alphanumeric'
|
|
||||||
});
|
|
||||||
let timeStamp = Date.now();
|
let timeStamp = Date.now();
|
||||||
let expirationTime;
|
let expirationTime;
|
||||||
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date
|
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date
|
||||||
|
|||||||
@@ -415,6 +415,11 @@ function includePath(config, auth, response, path) {
|
|||||||
for (var obj of includeResponse.results) {
|
for (var obj of includeResponse.results) {
|
||||||
obj.__type = 'Object';
|
obj.__type = 'Object';
|
||||||
obj.className = className;
|
obj.className = className;
|
||||||
|
|
||||||
|
if(className == "_User"){
|
||||||
|
delete obj.sessionToken;
|
||||||
|
}
|
||||||
|
|
||||||
replace[obj.objectId] = obj;
|
replace[obj.objectId] = obj;
|
||||||
}
|
}
|
||||||
var resp = {
|
var resp = {
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
// that writes to the database.
|
// that writes to the database.
|
||||||
// This could be either a "create" or an "update".
|
// This could be either a "create" or an "update".
|
||||||
|
|
||||||
var crypto = require('crypto');
|
|
||||||
var deepcopy = require('deepcopy');
|
var deepcopy = require('deepcopy');
|
||||||
var rack = require('hat').rack();
|
|
||||||
|
|
||||||
var Auth = require('./Auth');
|
var Auth = require('./Auth');
|
||||||
var cache = require('./cache');
|
var cache = require('./cache');
|
||||||
var Config = require('./Config');
|
var Config = require('./Config');
|
||||||
|
var cryptoUtils = require('./cryptoUtils');
|
||||||
var passwordCrypto = require('./password');
|
var passwordCrypto = require('./password');
|
||||||
var facebook = require('./facebook');
|
var facebook = require('./facebook');
|
||||||
var Parse = require('parse/node');
|
var Parse = require('parse/node');
|
||||||
@@ -57,7 +56,7 @@ function RestWrite(config, auth, className, query, data, originalData) {
|
|||||||
this.data.updatedAt = this.updatedAt;
|
this.data.updatedAt = this.updatedAt;
|
||||||
if (!this.query) {
|
if (!this.query) {
|
||||||
this.data.createdAt = this.updatedAt;
|
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,
|
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
|
||||||
'this auth is already used');
|
'this auth is already used');
|
||||||
} else {
|
} else {
|
||||||
this.data.username = rack();
|
this.data.username = cryptoUtils.newToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This FB auth does not already exist, so transform it to a
|
// This FB auth does not already exist, so transform it to a
|
||||||
@@ -289,7 +288,7 @@ RestWrite.prototype.transformUser = function() {
|
|||||||
var promise = Promise.resolve();
|
var promise = Promise.resolve();
|
||||||
|
|
||||||
if (!this.query) {
|
if (!this.query) {
|
||||||
var token = 'r:' + rack();
|
var token = 'r:' + cryptoUtils.newToken();
|
||||||
this.storage['token'] = token;
|
this.storage['token'] = token;
|
||||||
promise = promise.then(() => {
|
promise = promise.then(() => {
|
||||||
var expiresAt = new Date();
|
var expiresAt = new Date();
|
||||||
@@ -335,7 +334,7 @@ RestWrite.prototype.transformUser = function() {
|
|||||||
// Check for username uniqueness
|
// Check for username uniqueness
|
||||||
if (!this.data.username) {
|
if (!this.data.username) {
|
||||||
if (!this.query) {
|
if (!this.query) {
|
||||||
this.data.username = newStringId(25);
|
this.data.username = cryptoUtils.randomString(25);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -428,7 +427,7 @@ RestWrite.prototype.handleSession = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.query && !this.auth.isMaster) {
|
if (!this.query && !this.auth.isMaster) {
|
||||||
var token = 'r:' + rack();
|
var token = 'r:' + cryptoUtils.newToken();
|
||||||
var expiresAt = new Date();
|
var expiresAt = new Date();
|
||||||
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
|
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
|
||||||
var sessionData = {
|
var sessionData = {
|
||||||
@@ -721,20 +720,4 @@ RestWrite.prototype.objectId = function() {
|
|||||||
return this.data.objectId || this.query.objectId;
|
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;
|
module.exports = RestWrite;
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ export class ClassesRouter {
|
|||||||
if (!response.results || response.results.length == 0) {
|
if (!response.results || response.results.length == 0) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
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] };
|
return { response: response.results[0] };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ export class InstallationsRouter extends ClassesRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getExpressRouter() {
|
getExpressRouter() {
|
||||||
var router = new PromiseRouter();
|
let router = new PromiseRouter();
|
||||||
router.route('GET','/installations', (req) => { return this.handleFind(req); });
|
router.route('GET','/installations', req => { return this.handleFind(req); });
|
||||||
router.route('GET','/installations/:objectId', (req) => { return this.handleGet(req); });
|
router.route('GET','/installations/:objectId', req => { return this.handleGet(req); });
|
||||||
router.route('POST','/installations', (req) => { return this.handleCreate(req); });
|
router.route('POST','/installations', req => { return this.handleCreate(req); });
|
||||||
router.route('PUT','/installations/:objectId', (req) => { return this.handleUpdate(req); });
|
router.route('PUT','/installations/:objectId', req => { return this.handleUpdate(req); });
|
||||||
router.route('DELETE','/installations/:objectId', (req) => { return this.handleDelete(req); });
|
router.route('DELETE','/installations/:objectId', req => { return this.handleDelete(req); });
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/Routers/RolesRouter.js
Normal file
43
src/Routers/RolesRouter.js
Normal file
@@ -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;
|
||||||
63
src/Routers/SessionsRouter.js
Normal file
63
src/Routers/SessionsRouter.js
Normal file
@@ -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;
|
||||||
161
src/Routers/UsersRouter.js
Normal file
161
src/Routers/UsersRouter.js
Normal file
@@ -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;
|
||||||
44
src/cryptoUtils.js
Normal file
44
src/cryptoUtils.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ function handleCloudFunction(req) {
|
|||||||
params: req.body || {},
|
params: req.body || {},
|
||||||
master: req.auth && req.auth.isMaster,
|
master: req.auth && req.auth.isMaster,
|
||||||
user: req.auth && req.auth.user,
|
user: req.auth && req.auth.user,
|
||||||
|
installationId: req.info.installationId
|
||||||
};
|
};
|
||||||
Parse.Cloud.Functions[req.params.functionName](request, response);
|
Parse.Cloud.Functions[req.params.functionName](request, response);
|
||||||
});
|
});
|
||||||
|
|||||||
18
src/index.js
18
src/index.js
@@ -20,6 +20,12 @@ import { PushController } from './Controllers/PushController';
|
|||||||
|
|
||||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||||
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
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
|
// Mutate the Parse object to add the Cloud Code handlers
|
||||||
addParseCloud();
|
addParseCloud();
|
||||||
@@ -69,6 +75,9 @@ function ParseServer(args) {
|
|||||||
pushAdapter = new ParsePushAdapter(pushConfig)
|
pushAdapter = new ParsePushAdapter(pushConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make logger adapter
|
||||||
|
let loggerAdapter = args.loggerAdapter || new FileLoggerAdapter();
|
||||||
|
|
||||||
if (args.databaseURI) {
|
if (args.databaseURI) {
|
||||||
DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI);
|
DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI);
|
||||||
}
|
}
|
||||||
@@ -129,14 +138,15 @@ function ParseServer(args) {
|
|||||||
|
|
||||||
let routers = [
|
let routers = [
|
||||||
new ClassesRouter().getExpressRouter(),
|
new ClassesRouter().getExpressRouter(),
|
||||||
require('./users'),
|
new UsersRouter().getExpressRouter(),
|
||||||
require('./sessions'),
|
new SessionsRouter().getExpressRouter(),
|
||||||
require('./roles'),
|
new RolesRouter().getExpressRouter(),
|
||||||
require('./analytics'),
|
require('./analytics'),
|
||||||
new InstallationsRouter().getExpressRouter(),
|
new InstallationsRouter().getExpressRouter(),
|
||||||
require('./functions'),
|
require('./functions'),
|
||||||
require('./schemas'),
|
require('./schemas'),
|
||||||
new PushController(pushAdapter).getExpressRouter()
|
new PushController(pushAdapter).getExpressRouter(),
|
||||||
|
new LoggerController(loggerAdapter).getExpressRouter()
|
||||||
];
|
];
|
||||||
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
|
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
|
||||||
routers.push(require('./global_config'));
|
routers.push(require('./global_config'));
|
||||||
|
|||||||
@@ -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) {
|
function invalidRequest(req, res) {
|
||||||
res.status(403);
|
res.status(403);
|
||||||
res.end('{"error":"unauthorized"}');
|
res.end('{"error":"unauthorized"}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
allowCrossDomain: allowCrossDomain,
|
allowCrossDomain: allowCrossDomain,
|
||||||
allowMethodOverride: allowMethodOverride,
|
allowMethodOverride: allowMethodOverride,
|
||||||
handleParseErrors: handleParseErrors,
|
handleParseErrors: handleParseErrors,
|
||||||
handleParseHeaders: handleParseHeaders
|
handleParseHeaders: handleParseHeaders,
|
||||||
|
enforceMasterKeyAccess: enforceMasterKeyAccess
|
||||||
};
|
};
|
||||||
|
|||||||
48
src/roles.js
48
src/roles.js
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
var express = require('express'),
|
var express = require('express'),
|
||||||
cache = require('./cache'),
|
cache = require('./cache'),
|
||||||
middlewares = require('./middlewares'),
|
middlewares = require('./middlewares'),
|
||||||
rack = require('hat').rack();
|
cryptoUtils = require('./cryptoUtils');
|
||||||
|
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
|
|
||||||
// creates a unique app in the cache, with a collection prefix
|
// creates a unique app in the cache, with a collection prefix
|
||||||
function createApp(req, res) {
|
function createApp(req, res) {
|
||||||
var appId = rack();
|
var appId = cryptoUtils.randomHexString(32);
|
||||||
cache.apps[appId] = {
|
cache.apps[appId] = {
|
||||||
'collectionPrefix': appId + '_',
|
'collectionPrefix': appId + '_',
|
||||||
'masterKey': 'master'
|
'masterKey': 'master'
|
||||||
@@ -70,4 +70,4 @@ router.post('/rest_configure_app',
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
router: router
|
router: router
|
||||||
};
|
};
|
||||||
|
|||||||
212
src/users.js
212
src/users.js
@@ -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;
|
|
||||||
Reference in New Issue
Block a user