Merge remote-tracking branch 'upstream/master'
This commit is contained in:
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,5 +1,28 @@
|
|||||||
## Parse Server Changelog
|
## Parse Server Changelog
|
||||||
|
|
||||||
|
### 2.1.0 (2/17/2016)
|
||||||
|
|
||||||
|
* Feature: Support for additional OAuth providers
|
||||||
|
* Feature: Ability to implement custom OAuth providers
|
||||||
|
* Feature: Support for deleting Parse Files
|
||||||
|
* Feature: Allow querying roles
|
||||||
|
* Feature: Support for logs, extensible via Log Adapter
|
||||||
|
* Feature: New Push Adapter for sending push notifications through OneSignal
|
||||||
|
* Feature: Tighter default security for Users
|
||||||
|
* Feature: Pass parameters to Cloud Code in query string
|
||||||
|
* Feature: Disable anonymous users via configuration.
|
||||||
|
* Experimental: Schemas API support for PUT operations
|
||||||
|
* Fix: Prevent installation ID from being added to User
|
||||||
|
* Fix: Becoming a user works properly with sessions
|
||||||
|
* Fix: Including multiple object when some object are unavailable will get all the objects that are available
|
||||||
|
* Fix: Invalid URL for Parse Files
|
||||||
|
* Fix: Making a query without a limit now returns 100 results
|
||||||
|
* Fix: Expose installation id in cloud code
|
||||||
|
* Fix: Correct username for Anonymous users
|
||||||
|
* Fix: Session token issue after fetching user
|
||||||
|
* Fix: Issues during install process
|
||||||
|
* Fix: Issue with Unity SDK sending _noBody
|
||||||
|
|
||||||
### 2.0.8 (2/11/2016)
|
### 2.0.8 (2/11/2016)
|
||||||
|
|
||||||
* Add: support for Android and iOS push notifications
|
* Add: support for Android and iOS push notifications
|
||||||
|
|||||||
57
README.md
57
README.md
@@ -36,12 +36,67 @@ The client keys used with Parse are no longer necessary with parse-server. If y
|
|||||||
* restAPIKey
|
* restAPIKey
|
||||||
* dotNetKey
|
* dotNetKey
|
||||||
|
|
||||||
|
#### OAuth Support
|
||||||
|
|
||||||
|
parse-server supports 3rd party authentication with
|
||||||
|
|
||||||
|
* Twitter
|
||||||
|
* Meetup
|
||||||
|
* Linkedin
|
||||||
|
* Google
|
||||||
|
* Instagram
|
||||||
|
* Facebook
|
||||||
|
|
||||||
|
|
||||||
|
Configuration options for these 3rd-party modules is done with the oauth option passed to ParseServer:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
oauth: {
|
||||||
|
twitter: {
|
||||||
|
consumer_key: "", // REQUIRED
|
||||||
|
consumer_secret: "" // REQUIRED
|
||||||
|
},
|
||||||
|
facebook: {
|
||||||
|
appIds: "FACEBOOK APP ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom Authentication
|
||||||
|
|
||||||
|
It is possible to leverage the OAuth support with any 3rd party authentication that you bring in.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
|
||||||
|
oauth: {
|
||||||
|
my_custom_auth: {
|
||||||
|
module: "PATH_TO_MODULE" // OR object,
|
||||||
|
option1: "",
|
||||||
|
option2: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On this module, you need to implement and export those two functions `validateAuthData(authData, options) {} ` and `validateAppId(appIds, authData) {}`.
|
||||||
|
|
||||||
|
For more informations about custom auth please see the examples:
|
||||||
|
|
||||||
|
- [facebook OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/facebook.js)
|
||||||
|
- [twitter OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/twitter.js)
|
||||||
|
- [instagram OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/instagram.js)
|
||||||
|
|
||||||
|
|
||||||
#### Advanced options:
|
#### Advanced options:
|
||||||
|
|
||||||
* 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))
|
* 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))
|
* 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))
|
||||||
|
* enableAnonymousUsers - Defaults to true. Set to false to disable anonymous users.
|
||||||
---
|
---
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ if (process.env.PARSE_SERVER_OPTIONS) {
|
|||||||
facebookAppIds = facebookAppIds.split(",");
|
facebookAppIds = facebookAppIds.split(",");
|
||||||
options.facebookAppIds = facebookAppIds;
|
options.facebookAppIds = facebookAppIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var oauth = process.env.PARSE_SERVER_OAUTH_PROVIDERS;
|
||||||
|
if (oauth) {
|
||||||
|
options.oauth = JSON.parse(oauth);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var mountPath = process.env.PARSE_SERVER_MOUNT_PATH || "/";
|
var mountPath = process.env.PARSE_SERVER_MOUNT_PATH || "/";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "parse-server",
|
"name": "parse-server",
|
||||||
"version": "2.0.8",
|
"version": "2.1.0",
|
||||||
"description": "An express module providing a Parse-compatible API server",
|
"description": "An express module providing a Parse-compatible API server",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
307
spec/OAuth.spec.js
Normal file
307
spec/OAuth.spec.js
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
var OAuth = require("../src/oauth/OAuth1Client");
|
||||||
|
var request = require('request');
|
||||||
|
|
||||||
|
describe('OAuth', function() {
|
||||||
|
|
||||||
|
it("Nonce should have right length", (done) => {
|
||||||
|
jequal(OAuth.nonce().length, 30);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should properly build parameter string", (done) => {
|
||||||
|
var string = OAuth.buildParameterString({c:1, a:2, b:3})
|
||||||
|
jequal(string, "a=2&b=3&c=1");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should properly build empty parameter string", (done) => {
|
||||||
|
var string = OAuth.buildParameterString()
|
||||||
|
jequal(string, "");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should properly build signature string", (done) => {
|
||||||
|
var string = OAuth.buildSignatureString("get", "http://dummy.com", "");
|
||||||
|
jequal(string, "GET&http%3A%2F%2Fdummy.com&");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should properly generate request signature", (done) => {
|
||||||
|
var request = {
|
||||||
|
host: "dummy.com",
|
||||||
|
path: "path"
|
||||||
|
};
|
||||||
|
|
||||||
|
var oauth_params = {
|
||||||
|
oauth_timestamp: 123450000,
|
||||||
|
oauth_nonce: "AAAAAAAAAAAAAAAAA",
|
||||||
|
oauth_consumer_key: "hello",
|
||||||
|
oauth_token: "token"
|
||||||
|
};
|
||||||
|
|
||||||
|
var consumer_secret = "world";
|
||||||
|
var auth_token_secret = "secret";
|
||||||
|
request = OAuth.signRequest(request, oauth_params, consumer_secret, auth_token_secret);
|
||||||
|
jequal(request.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should properly build request", (done) => {
|
||||||
|
var options = {
|
||||||
|
host: "dummy.com",
|
||||||
|
consumer_key: "hello",
|
||||||
|
consumer_secret: "world",
|
||||||
|
auth_token: "token",
|
||||||
|
auth_token_secret: "secret",
|
||||||
|
// Custom oauth params for tests
|
||||||
|
oauth_params: {
|
||||||
|
oauth_timestamp: 123450000,
|
||||||
|
oauth_nonce: "AAAAAAAAAAAAAAAAA"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var path = "path";
|
||||||
|
var method = "get";
|
||||||
|
|
||||||
|
var oauthClient = new OAuth(options);
|
||||||
|
var req = oauthClient.buildRequest(method, path, {"query": "param"});
|
||||||
|
|
||||||
|
jequal(req.host, options.host);
|
||||||
|
jequal(req.path, "/"+path+"?query=param");
|
||||||
|
jequal(req.method, "GET");
|
||||||
|
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
|
||||||
|
jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"')
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function validateCannotAuthenticateError(data, done) {
|
||||||
|
jequal(typeof data, "object");
|
||||||
|
jequal(typeof data.errors, "object");
|
||||||
|
var errors = data.errors;
|
||||||
|
jequal(typeof errors[0], "object");
|
||||||
|
// Cannot authenticate error
|
||||||
|
jequal(errors[0].code, 32);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
it("Should fail a GET request", (done) => {
|
||||||
|
var options = {
|
||||||
|
host: "api.twitter.com",
|
||||||
|
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
};
|
||||||
|
var path = "/1.1/help/configuration.json";
|
||||||
|
var params = {"lang": "en"};
|
||||||
|
var oauthClient = new OAuth(options);
|
||||||
|
oauthClient.get(path, params).then(function(data){
|
||||||
|
validateCannotAuthenticateError(data, done);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should fail a POST request", (done) => {
|
||||||
|
var options = {
|
||||||
|
host: "api.twitter.com",
|
||||||
|
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
};
|
||||||
|
var body = {
|
||||||
|
lang: "en"
|
||||||
|
};
|
||||||
|
var path = "/1.1/account/settings.json";
|
||||||
|
|
||||||
|
var oauthClient = new OAuth(options);
|
||||||
|
oauthClient.post(path, null, body).then(function(data){
|
||||||
|
validateCannotAuthenticateError(data, done);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should fail a request", (done) => {
|
||||||
|
var options = {
|
||||||
|
host: "localhost",
|
||||||
|
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
};
|
||||||
|
var body = {
|
||||||
|
lang: "en"
|
||||||
|
};
|
||||||
|
var path = "/";
|
||||||
|
|
||||||
|
var oauthClient = new OAuth(options);
|
||||||
|
oauthClient.post(path, null, body).then(function(data){
|
||||||
|
jequal(false, true);
|
||||||
|
done();
|
||||||
|
}).catch(function(){
|
||||||
|
jequal(true, true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter"].map(function(providerName){
|
||||||
|
it("Should validate structure of "+providerName, (done) => {
|
||||||
|
var provider = require("../src/oauth/"+providerName);
|
||||||
|
jequal(typeof provider.validateAuthData, "function");
|
||||||
|
jequal(typeof provider.validateAppId, "function");
|
||||||
|
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
|
||||||
|
jequal(provider.validateAppId("app", "key", {}).constructor, Promise.prototype.constructor);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var getMockMyOauthProvider = function() {
|
||||||
|
return {
|
||||||
|
authData: {
|
||||||
|
id: "12345",
|
||||||
|
access_token: "12345",
|
||||||
|
expiration_date: new Date().toJSON(),
|
||||||
|
},
|
||||||
|
shouldError: false,
|
||||||
|
loggedOut: false,
|
||||||
|
synchronizedUserId: null,
|
||||||
|
synchronizedAuthToken: null,
|
||||||
|
synchronizedExpiration: null,
|
||||||
|
|
||||||
|
authenticate: function(options) {
|
||||||
|
if (this.shouldError) {
|
||||||
|
options.error(this, "An error occurred");
|
||||||
|
} else if (this.shouldCancel) {
|
||||||
|
options.error(this, null);
|
||||||
|
} else {
|
||||||
|
options.success(this, this.authData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restoreAuthentication: function(authData) {
|
||||||
|
if (!authData) {
|
||||||
|
this.synchronizedUserId = null;
|
||||||
|
this.synchronizedAuthToken = null;
|
||||||
|
this.synchronizedExpiration = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.synchronizedUserId = authData.id;
|
||||||
|
this.synchronizedAuthToken = authData.access_token;
|
||||||
|
this.synchronizedExpiration = authData.expiration_date;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getAuthType: function() {
|
||||||
|
return "myoauth";
|
||||||
|
},
|
||||||
|
deauthenticate: function() {
|
||||||
|
this.loggedOut = true;
|
||||||
|
this.restoreAuthentication(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var ExtendedUser = Parse.User.extend({
|
||||||
|
extended: function() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var createOAuthUser = function(callback) {
|
||||||
|
var jsonBody = {
|
||||||
|
authData: {
|
||||||
|
myoauth: getMockMyOauthProvider().authData
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var headers = {'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest',
|
||||||
|
'Content-Type': 'application/json' }
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
headers: {'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest',
|
||||||
|
'Content-Type': 'application/json' },
|
||||||
|
url: 'http://localhost:8378/1/users',
|
||||||
|
body: JSON.stringify(jsonBody)
|
||||||
|
};
|
||||||
|
|
||||||
|
return request.post(options, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should create user with REST API", (done) => {
|
||||||
|
|
||||||
|
createOAuthUser((error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.objectId).not.toBeNull();
|
||||||
|
expect(b.objectId).not.toBeUndefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only create a single user with REST API", (done) => {
|
||||||
|
var objectId;
|
||||||
|
createOAuthUser((error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.objectId).not.toBeNull();
|
||||||
|
expect(b.objectId).not.toBeUndefined();
|
||||||
|
objectId = b.objectId;
|
||||||
|
|
||||||
|
createOAuthUser((error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.objectId).not.toBeNull();
|
||||||
|
expect(b.objectId).not.toBeUndefined();
|
||||||
|
expect(b.objectId).toBe(objectId);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unlink and link with custom provider", (done) => {
|
||||||
|
var provider = getMockMyOauthProvider();
|
||||||
|
Parse.User._registerAuthenticationProvider(provider);
|
||||||
|
Parse.User._logInWith("myoauth", {
|
||||||
|
success: function(model) {
|
||||||
|
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
||||||
|
strictEqual(Parse.User.current(), model);
|
||||||
|
ok(model.extended(), "Should have used the subclass.");
|
||||||
|
strictEqual(provider.authData.id, provider.synchronizedUserId);
|
||||||
|
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
|
||||||
|
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
|
||||||
|
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
|
||||||
|
|
||||||
|
model._unlinkFrom("myoauth", {
|
||||||
|
success: function(model) {
|
||||||
|
ok(!model._isLinked("myoauth"),
|
||||||
|
"User should not be linked to myoauth");
|
||||||
|
ok(!provider.synchronizedUserId, "User id should be cleared");
|
||||||
|
ok(!provider.synchronizedAuthToken, "Auth token should be cleared");
|
||||||
|
ok(!provider.synchronizedExpiration,
|
||||||
|
"Expiration should be cleared");
|
||||||
|
|
||||||
|
model._linkWith("myoauth", {
|
||||||
|
success: function(model) {
|
||||||
|
ok(provider.synchronizedUserId, "User id should have a value");
|
||||||
|
ok(provider.synchronizedAuthToken,
|
||||||
|
"Auth token should have a value");
|
||||||
|
ok(provider.synchronizedExpiration,
|
||||||
|
"Expiration should have a value");
|
||||||
|
ok(model._isLinked("myoauth"),
|
||||||
|
"User should be linked to myoauth");
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
error: function(model, error) {
|
||||||
|
ok(false, "linking again should succeed");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(model, error) {
|
||||||
|
ok(false, "unlinking should succeed");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(model, error) {
|
||||||
|
ok(false, "linking should have worked");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
234
spec/OneSignalPushAdapter.spec.js
Normal file
234
spec/OneSignalPushAdapter.spec.js
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
|
||||||
|
var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter');
|
||||||
|
|
||||||
|
describe('OneSignalPushAdapter', () => {
|
||||||
|
it('can be initialized', (done) => {
|
||||||
|
// Make mock config
|
||||||
|
var pushConfig = {
|
||||||
|
oneSignalAppId:"APP ID",
|
||||||
|
oneSignalApiKey:"API KEY"
|
||||||
|
};
|
||||||
|
|
||||||
|
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||||
|
|
||||||
|
var senderMap = oneSignalPushAdapter.senderMap;
|
||||||
|
|
||||||
|
expect(senderMap.ios instanceof Function).toBe(true);
|
||||||
|
expect(senderMap.android instanceof Function).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can get valid push types', (done) => {
|
||||||
|
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||||
|
|
||||||
|
expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can classify installation', (done) => {
|
||||||
|
// Mock installations
|
||||||
|
var validPushTypes = ['ios', 'android'];
|
||||||
|
var installations = [
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: 'androidToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'ios',
|
||||||
|
deviceToken: 'iosToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'win',
|
||||||
|
deviceToken: 'winToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: undefined
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var deviceMap = OneSignalPushAdapter.classifyInstallation(installations, validPushTypes);
|
||||||
|
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
|
||||||
|
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
|
||||||
|
expect(deviceMap['win']).toBe(undefined);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('can send push notifications', (done) => {
|
||||||
|
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||||
|
|
||||||
|
// Mock android ios senders
|
||||||
|
var androidSender = jasmine.createSpy('send')
|
||||||
|
var iosSender = jasmine.createSpy('send')
|
||||||
|
|
||||||
|
var senderMap = {
|
||||||
|
ios: iosSender,
|
||||||
|
android: androidSender
|
||||||
|
};
|
||||||
|
oneSignalPushAdapter.senderMap = senderMap;
|
||||||
|
|
||||||
|
// Mock installations
|
||||||
|
var installations = [
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: 'androidToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'ios',
|
||||||
|
deviceToken: 'iosToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'win',
|
||||||
|
deviceToken: 'winToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: undefined
|
||||||
|
}
|
||||||
|
];
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
oneSignalPushAdapter.send(data, installations);
|
||||||
|
// Check android sender
|
||||||
|
expect(androidSender).toHaveBeenCalled();
|
||||||
|
var args = androidSender.calls.first().args;
|
||||||
|
expect(args[0]).toEqual(data);
|
||||||
|
expect(args[1]).toEqual([
|
||||||
|
makeDevice('androidToken')
|
||||||
|
]);
|
||||||
|
// Check ios sender
|
||||||
|
expect(iosSender).toHaveBeenCalled();
|
||||||
|
args = iosSender.calls.first().args;
|
||||||
|
expect(args[0]).toEqual(data);
|
||||||
|
expect(args[1]).toEqual([
|
||||||
|
makeDevice('iosToken')
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can send iOS notifications", (done) => {
|
||||||
|
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||||
|
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
|
||||||
|
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
|
||||||
|
|
||||||
|
oneSignalPushAdapter.sendToAPNS({'data':{
|
||||||
|
'badge': 1,
|
||||||
|
'alert': "Example content",
|
||||||
|
'sound': "Example sound",
|
||||||
|
'content-available': 1,
|
||||||
|
'misc-data': 'Example Data'
|
||||||
|
}},[{'deviceToken':'iosToken1'},{'deviceToken':'iosToken2'}])
|
||||||
|
|
||||||
|
expect(sendToOneSignal).toHaveBeenCalled();
|
||||||
|
var args = sendToOneSignal.calls.first().args;
|
||||||
|
expect(args[0]).toEqual({
|
||||||
|
'ios_badgeType':'SetTo',
|
||||||
|
'ios_badgeCount':1,
|
||||||
|
'contents': { 'en':'Example content'},
|
||||||
|
'ios_sound': 'Example sound',
|
||||||
|
'content_available':true,
|
||||||
|
'data':{'misc-data':'Example Data'},
|
||||||
|
'include_ios_tokens':['iosToken1','iosToken2']
|
||||||
|
})
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can send Android notifications", (done) => {
|
||||||
|
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||||
|
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
|
||||||
|
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
|
||||||
|
|
||||||
|
oneSignalPushAdapter.sendToGCM({'data':{
|
||||||
|
'title': 'Example title',
|
||||||
|
'alert': 'Example content',
|
||||||
|
'misc-data': 'Example Data'
|
||||||
|
}},[{'deviceToken':'androidToken1'},{'deviceToken':'androidToken2'}])
|
||||||
|
|
||||||
|
expect(sendToOneSignal).toHaveBeenCalled();
|
||||||
|
var args = sendToOneSignal.calls.first().args;
|
||||||
|
expect(args[0]).toEqual({
|
||||||
|
'contents': { 'en':'Example content'},
|
||||||
|
'title': {'en':'Example title'},
|
||||||
|
'data':{'misc-data':'Example Data'},
|
||||||
|
'include_android_reg_ids': ['androidToken1','androidToken2']
|
||||||
|
})
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can post the correct data", (done) => {
|
||||||
|
var pushConfig = {
|
||||||
|
oneSignalAppId:"APP ID",
|
||||||
|
oneSignalApiKey:"API KEY"
|
||||||
|
};
|
||||||
|
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||||
|
|
||||||
|
var write = jasmine.createSpy('write');
|
||||||
|
oneSignalPushAdapter.https = {
|
||||||
|
'request': function(a,b) {
|
||||||
|
return {
|
||||||
|
'end':function(){},
|
||||||
|
'on':function(a,b){},
|
||||||
|
'write':write
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var installations = [
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: 'androidToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'ios',
|
||||||
|
deviceToken: 'iosToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'win',
|
||||||
|
deviceToken: 'winToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: undefined
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
oneSignalPushAdapter.send({'data':{
|
||||||
|
'title': 'Example title',
|
||||||
|
'alert': 'Example content',
|
||||||
|
'content-available':1,
|
||||||
|
'misc-data': 'Example Data'
|
||||||
|
}}, installations);
|
||||||
|
|
||||||
|
expect(write).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// iOS
|
||||||
|
args = write.calls.first().args;
|
||||||
|
expect(args[0]).toEqual(JSON.stringify({
|
||||||
|
'contents': { 'en':'Example content'},
|
||||||
|
'content_available':true,
|
||||||
|
'data':{'title':'Example title','misc-data':'Example Data'},
|
||||||
|
'include_ios_tokens':['iosToken'],
|
||||||
|
'app_id':'APP ID'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Android
|
||||||
|
args = write.calls.mostRecent().args;
|
||||||
|
expect(args[0]).toEqual(JSON.stringify({
|
||||||
|
'contents': { 'en':'Example content'},
|
||||||
|
'title': {'en':'Example title'},
|
||||||
|
'data':{"content-available":1,'misc-data':'Example Data'},
|
||||||
|
'include_android_reg_ids':['androidToken'],
|
||||||
|
'app_id':'APP ID'
|
||||||
|
}));
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeDevice(deviceToken, appIdentifier) {
|
||||||
|
return {
|
||||||
|
deviceToken: deviceToken
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -129,6 +129,22 @@ describe('miscellaneous', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('query without limit get default 100 records', function(done) {
|
||||||
|
var objects = [];
|
||||||
|
for (var i = 0; i < 150; i++) {
|
||||||
|
objects.push(new TestObject({name: 'name' + i}));
|
||||||
|
}
|
||||||
|
Parse.Object.saveAll(objects).then(() => {
|
||||||
|
return new Parse.Query(TestObject).find();
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(100);
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('basic saveAll', function(done) {
|
it('basic saveAll', function(done) {
|
||||||
var alpha = new TestObject({ letter: 'alpha' });
|
var alpha = new TestObject({ letter: 'alpha' });
|
||||||
var beta = new TestObject({ letter: 'beta' });
|
var beta = new TestObject({ letter: 'beta' });
|
||||||
@@ -571,6 +587,35 @@ describe('miscellaneous', function() {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('test cloud function query parameters', (done) => {
|
||||||
|
Parse.Cloud.define('echoParams', (req, res) => {
|
||||||
|
res.success(req.params);
|
||||||
|
});
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-Javascript-Key': 'test'
|
||||||
|
};
|
||||||
|
request.post({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/functions/echoParams', //?option=1&other=2
|
||||||
|
qs: {
|
||||||
|
option: 1,
|
||||||
|
other: 2
|
||||||
|
},
|
||||||
|
body: '{"foo":"bar", "other": 1}'
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var res = JSON.parse(body).result;
|
||||||
|
expect(res.option).toEqual('1');
|
||||||
|
// Make sure query string params override body params
|
||||||
|
expect(res.other).toEqual('2');
|
||||||
|
expect(res.foo).toEqual("bar");
|
||||||
|
delete Parse.Cloud.Functions['echoParams'];
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('test cloud function parameter validation success', (done) => {
|
it('test cloud function parameter validation success', (done) => {
|
||||||
// Register a function with validation
|
// Register a function with validation
|
||||||
|
|||||||
@@ -133,26 +133,6 @@ describe('Installations', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails for android with device token', (done) => {
|
|
||||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
|
||||||
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
|
||||||
var device = 'android';
|
|
||||||
var input = {
|
|
||||||
'installationId': installId,
|
|
||||||
'deviceType': device,
|
|
||||||
'deviceToken': t,
|
|
||||||
'channels': ['foo', 'bar']
|
|
||||||
};
|
|
||||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
|
||||||
.then(() => {
|
|
||||||
fail('Should not have been able to create an Installation.');
|
|
||||||
done();
|
|
||||||
}).catch((error) => {
|
|
||||||
expect(error.code).toEqual(114);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails for android with missing type', (done) => {
|
it('fails for android with missing type', (done) => {
|
||||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
var input = {
|
var input = {
|
||||||
|
|||||||
@@ -8,6 +8,20 @@
|
|||||||
var request = require('request');
|
var request = require('request');
|
||||||
var passwordCrypto = require('../src/password');
|
var passwordCrypto = require('../src/password');
|
||||||
|
|
||||||
|
function verifyACL(user) {
|
||||||
|
const ACL = user.getACL();
|
||||||
|
expect(ACL.getReadAccess(user)).toBe(true);
|
||||||
|
expect(ACL.getWriteAccess(user)).toBe(true);
|
||||||
|
expect(ACL.getPublicReadAccess()).toBe(true);
|
||||||
|
expect(ACL.getPublicWriteAccess()).toBe(false);
|
||||||
|
const perms = ACL.permissionsById;
|
||||||
|
expect(Object.keys(perms).length).toBe(2);
|
||||||
|
expect(perms[user.id].read).toBe(true);
|
||||||
|
expect(perms[user.id].write).toBe(true);
|
||||||
|
expect(perms['*'].read).toBe(true);
|
||||||
|
expect(perms['*'].write).not.toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
describe('Parse.User testing', () => {
|
describe('Parse.User testing', () => {
|
||||||
it("user sign up class method", (done) => {
|
it("user sign up class method", (done) => {
|
||||||
Parse.User.signUp("asdf", "zxcv", null, {
|
Parse.User.signUp("asdf", "zxcv", null, {
|
||||||
@@ -57,6 +71,7 @@ describe('Parse.User testing', () => {
|
|||||||
Parse.User.logIn("asdf", "zxcv", {
|
Parse.User.logIn("asdf", "zxcv", {
|
||||||
success: function(user) {
|
success: function(user) {
|
||||||
equal(user.get("username"), "asdf");
|
equal(user.get("username"), "asdf");
|
||||||
|
verifyACL(user);
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -816,9 +831,11 @@ describe('Parse.User testing', () => {
|
|||||||
// server-side.
|
// server-side.
|
||||||
var getMockFacebookProvider = function() {
|
var getMockFacebookProvider = function() {
|
||||||
return {
|
return {
|
||||||
userId: "8675309",
|
authData: {
|
||||||
authToken: "jenny",
|
id: "8675309",
|
||||||
expiration: new Date().toJSON(),
|
access_token: "jenny",
|
||||||
|
expiration_date: new Date().toJSON(),
|
||||||
|
},
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
loggedOut: false,
|
loggedOut: false,
|
||||||
synchronizedUserId: null,
|
synchronizedUserId: null,
|
||||||
@@ -831,11 +848,7 @@ describe('Parse.User testing', () => {
|
|||||||
} else if (this.shouldCancel) {
|
} else if (this.shouldCancel) {
|
||||||
options.error(this, null);
|
options.error(this, null);
|
||||||
} else {
|
} else {
|
||||||
options.success(this, {
|
options.success(this, this.authData);
|
||||||
id: this.userId,
|
|
||||||
access_token: this.authToken,
|
|
||||||
expiration_date: this.expiration
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
restoreAuthentication: function(authData) {
|
restoreAuthentication: function(authData) {
|
||||||
@@ -874,13 +887,14 @@ describe('Parse.User testing', () => {
|
|||||||
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
||||||
strictEqual(Parse.User.current(), model);
|
strictEqual(Parse.User.current(), model);
|
||||||
ok(model.extended(), "Should have used subclass.");
|
ok(model.extended(), "Should have used subclass.");
|
||||||
strictEqual(provider.userId, provider.synchronizedUserId);
|
strictEqual(provider.authData.id, provider.synchronizedUserId);
|
||||||
strictEqual(provider.authToken, provider.synchronizedAuthToken);
|
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
|
||||||
strictEqual(provider.expiration, provider.synchronizedExpiration);
|
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
|
||||||
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
error: function(model, error) {
|
error: function(model, error) {
|
||||||
|
console.error(model, error);
|
||||||
ok(false, "linking should have worked");
|
ok(false, "linking should have worked");
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
@@ -895,9 +909,9 @@ describe('Parse.User testing', () => {
|
|||||||
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
||||||
strictEqual(Parse.User.current(), model);
|
strictEqual(Parse.User.current(), model);
|
||||||
ok(model.extended(), "Should have used the subclass.");
|
ok(model.extended(), "Should have used the subclass.");
|
||||||
strictEqual(provider.userId, provider.synchronizedUserId);
|
strictEqual(provider.authData.id, provider.synchronizedUserId);
|
||||||
strictEqual(provider.authToken, provider.synchronizedAuthToken);
|
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
|
||||||
strictEqual(provider.expiration, provider.synchronizedExpiration);
|
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
|
||||||
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
||||||
|
|
||||||
Parse.User.logOut();
|
Parse.User.logOut();
|
||||||
@@ -910,20 +924,22 @@ describe('Parse.User testing', () => {
|
|||||||
"Model should be a Parse.User");
|
"Model should be a Parse.User");
|
||||||
ok(innerModel === Parse.User.current(),
|
ok(innerModel === Parse.User.current(),
|
||||||
"Returned model should be the current user");
|
"Returned model should be the current user");
|
||||||
ok(provider.userId === provider.synchronizedUserId);
|
ok(provider.authData.id === provider.synchronizedUserId);
|
||||||
ok(provider.authToken === provider.synchronizedAuthToken);
|
ok(provider.authData.access_token === provider.synchronizedAuthToken);
|
||||||
ok(innerModel._isLinked("facebook"),
|
ok(innerModel._isLinked("facebook"),
|
||||||
"User should be linked to facebook");
|
"User should be linked to facebook");
|
||||||
ok(innerModel.existed(), "User should not be newly-created");
|
ok(innerModel.existed(), "User should not be newly-created");
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
error: function(model, error) {
|
error: function(model, error) {
|
||||||
|
fail(error);
|
||||||
ok(false, "LogIn should have worked");
|
ok(false, "LogIn should have worked");
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: function(model, error) {
|
error: function(model, error) {
|
||||||
|
console.error(model, error);
|
||||||
ok(false, "LogIn should have worked");
|
ok(false, "LogIn should have worked");
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
@@ -972,9 +988,9 @@ describe('Parse.User testing', () => {
|
|||||||
success: function(model) {
|
success: function(model) {
|
||||||
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
||||||
strictEqual(Parse.User.current(), model);
|
strictEqual(Parse.User.current(), model);
|
||||||
strictEqual(provider.userId, provider.synchronizedUserId);
|
strictEqual(provider.authData.id, provider.synchronizedUserId);
|
||||||
strictEqual(provider.authToken, provider.synchronizedAuthToken);
|
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
|
||||||
strictEqual(provider.expiration, provider.synchronizedExpiration);
|
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
|
||||||
ok(model._isLinked("facebook"), "User should be linked");
|
ok(model._isLinked("facebook"), "User should be linked");
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
@@ -1005,9 +1021,9 @@ describe('Parse.User testing', () => {
|
|||||||
success: function(model) {
|
success: function(model) {
|
||||||
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
||||||
strictEqual(Parse.User.current(), model);
|
strictEqual(Parse.User.current(), model);
|
||||||
strictEqual(provider.userId, provider.synchronizedUserId);
|
strictEqual(provider.authData.id, provider.synchronizedUserId);
|
||||||
strictEqual(provider.authToken, provider.synchronizedAuthToken);
|
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
|
||||||
strictEqual(provider.expiration, provider.synchronizedExpiration);
|
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
|
||||||
ok(model._isLinked("facebook"), "User should be linked.");
|
ok(model._isLinked("facebook"), "User should be linked.");
|
||||||
var user2 = new Parse.User();
|
var user2 = new Parse.User();
|
||||||
user2.set("username", "testLinkWithProviderToAlreadyLinkedUser2");
|
user2.set("username", "testLinkWithProviderToAlreadyLinkedUser2");
|
||||||
@@ -1108,9 +1124,9 @@ describe('Parse.User testing', () => {
|
|||||||
ok(model instanceof Parse.User, "Model should be a Parse.User.");
|
ok(model instanceof Parse.User, "Model should be a Parse.User.");
|
||||||
strictEqual(Parse.User.current(), model);
|
strictEqual(Parse.User.current(), model);
|
||||||
ok(model.extended(), "Should have used the subclass.");
|
ok(model.extended(), "Should have used the subclass.");
|
||||||
strictEqual(provider.userId, provider.synchronizedUserId);
|
strictEqual(provider.authData.id, provider.synchronizedUserId);
|
||||||
strictEqual(provider.authToken, provider.synchronizedAuthToken);
|
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
|
||||||
strictEqual(provider.expiration, provider.synchronizedExpiration);
|
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
|
||||||
ok(model._isLinked("facebook"), "User should be linked to facebook.");
|
ok(model._isLinked("facebook"), "User should be linked to facebook.");
|
||||||
|
|
||||||
model._unlinkFrom("facebook", {
|
model._unlinkFrom("facebook", {
|
||||||
@@ -1144,9 +1160,9 @@ describe('Parse.User testing', () => {
|
|||||||
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
||||||
strictEqual(Parse.User.current(), model);
|
strictEqual(Parse.User.current(), model);
|
||||||
ok(model.extended(), "Should have used the subclass.");
|
ok(model.extended(), "Should have used the subclass.");
|
||||||
strictEqual(provider.userId, provider.synchronizedUserId);
|
strictEqual(provider.authData.id, provider.synchronizedUserId);
|
||||||
strictEqual(provider.authToken, provider.synchronizedAuthToken);
|
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
|
||||||
strictEqual(provider.expiration, provider.synchronizedExpiration);
|
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
|
||||||
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
||||||
|
|
||||||
model._unlinkFrom("facebook", {
|
model._unlinkFrom("facebook", {
|
||||||
@@ -1358,6 +1374,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');
|
||||||
@@ -1587,7 +1622,30 @@ describe('Parse.User testing', () => {
|
|||||||
}).then(function(newUser) {
|
}).then(function(newUser) {
|
||||||
fail('Session should have been invalidated');
|
fail('Session should have been invalidated');
|
||||||
done();
|
done();
|
||||||
}, function() {
|
}, function(err) {
|
||||||
|
expect(err.code).toBe(Parse.Error.INVALID_SESSION_TOKEN);
|
||||||
|
expect(err.message).toBe('invalid session token');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test parse user become', (done) => {
|
||||||
|
var sessionToken = null;
|
||||||
|
Parse.Promise.as().then(function() {
|
||||||
|
return Parse.User.signUp("flessard", "folo",{'foo':1});
|
||||||
|
}).then(function(newUser) {
|
||||||
|
equal(Parse.User.current(), newUser);
|
||||||
|
sessionToken = newUser.getSessionToken();
|
||||||
|
ok(sessionToken);
|
||||||
|
newUser.set('foo',2);
|
||||||
|
return newUser.save();
|
||||||
|
}).then(function() {
|
||||||
|
return Parse.User.become(sessionToken);
|
||||||
|
}).then(function(newUser) {
|
||||||
|
equal(newUser.get('foo'), 2);
|
||||||
|
done();
|
||||||
|
}, function(e) {
|
||||||
|
fail('The session should still be valid');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,6 +100,25 @@ describe('rest create', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles no anonymous users config', (done) => {
|
||||||
|
var NoAnnonConfig = Object.assign({}, config, {enableAnonymousUsers: false});
|
||||||
|
var data1 = {
|
||||||
|
authData: {
|
||||||
|
anonymous: {
|
||||||
|
id: '00000000-0000-0000-0000-000000000001'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rest.create(NoAnnonConfig, auth.nobody(NoAnnonConfig), '_User', data1).then(() => {
|
||||||
|
fail("Should throw an error");
|
||||||
|
done();
|
||||||
|
}, (err) => {
|
||||||
|
expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE);
|
||||||
|
expect(err.message).toEqual('This authentication method is unsupported.');
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
it('test facebook signup and login', (done) => {
|
it('test facebook signup and login', (done) => {
|
||||||
var data = {
|
var data = {
|
||||||
|
|||||||
@@ -162,6 +162,9 @@ describe('Schema', () => {
|
|||||||
foo: 'string',
|
foo: 'string',
|
||||||
})
|
})
|
||||||
done();
|
done();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
fail('Error creating class: ' + JSON.stringify(error));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -570,4 +573,32 @@ describe('Schema', () => {
|
|||||||
Parse.Object.enableSingleInstance();
|
Parse.Object.enableSingleInstance();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can merge schemas', done => {
|
||||||
|
expect(Schema.buildMergedSchemaObject({
|
||||||
|
_id: 'SomeClass',
|
||||||
|
someType: 'number'
|
||||||
|
}, {
|
||||||
|
newType: {type: 'Number'}
|
||||||
|
})).toEqual({
|
||||||
|
someType: {type: 'Number'},
|
||||||
|
newType: {type: 'Number'},
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can merge deletions', done => {
|
||||||
|
expect(Schema.buildMergedSchemaObject({
|
||||||
|
_id: 'SomeClass',
|
||||||
|
someType: 'number',
|
||||||
|
outDatedType: 'string',
|
||||||
|
},{
|
||||||
|
newType: {type: 'GeoPoint'},
|
||||||
|
outDatedType: {__op: 'Delete'},
|
||||||
|
})).toEqual({
|
||||||
|
someType: {type: 'Number'},
|
||||||
|
newType: {type: 'GeoPoint'},
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;
|
|||||||
var cache = require('../src/cache');
|
var cache = require('../src/cache');
|
||||||
var DatabaseAdapter = require('../src/DatabaseAdapter');
|
var DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
var facebook = require('../src/facebook');
|
var facebook = require('../src/oauth/facebook');
|
||||||
var ParseServer = require('../src/index').ParseServer;
|
var ParseServer = require('../src/index').ParseServer;
|
||||||
|
|
||||||
var databaseURI = process.env.DATABASE_URI;
|
var databaseURI = process.env.DATABASE_URI;
|
||||||
@@ -22,7 +22,13 @@ var api = new ParseServer({
|
|||||||
restAPIKey: 'rest',
|
restAPIKey: 'rest',
|
||||||
masterKey: 'test',
|
masterKey: 'test',
|
||||||
collectionPrefix: 'test_',
|
collectionPrefix: 'test_',
|
||||||
fileKey: 'test'
|
fileKey: 'test',
|
||||||
|
oauth: { // Override the facebook provider
|
||||||
|
facebook: mockFacebook(),
|
||||||
|
myoauth: {
|
||||||
|
module: "../spec/myoauth" // relative path as it's run from src
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
@@ -40,7 +46,6 @@ Parse.Promise.disableAPlusCompliant();
|
|||||||
|
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
Parse.initialize('test', 'test', 'test');
|
Parse.initialize('test', 'test', 'test');
|
||||||
mockFacebook();
|
|
||||||
Parse.User.enableUnsafeCurrentUser();
|
Parse.User.enableUnsafeCurrentUser();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -175,18 +180,20 @@ function range(n) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mockFacebook() {
|
function mockFacebook() {
|
||||||
facebook.validateUserId = function(userId, accessToken) {
|
var facebook = {};
|
||||||
if (userId === '8675309' && accessToken === 'jenny') {
|
facebook.validateAuthData = function(authData) {
|
||||||
|
if (authData.id === '8675309' && authData.access_token === 'jenny') {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
};
|
};
|
||||||
facebook.validateAppId = function(appId, accessToken) {
|
facebook.validateAppId = function(appId, authData) {
|
||||||
if (accessToken === 'jenny') {
|
if (authData.access_token === 'jenny') {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
};
|
};
|
||||||
|
return facebook;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearData() {
|
function clearData() {
|
||||||
|
|||||||
17
spec/myoauth.js
Normal file
17
spec/myoauth.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Custom oauth provider by module
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateAuthData(authData) {
|
||||||
|
if (authData.id == "12345" && authData.access_token == "12345") {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
function validateAppId() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateAppId: validateAppId,
|
||||||
|
validateAuthData: validateAuthData
|
||||||
|
};
|
||||||
@@ -94,7 +94,7 @@ describe('schemas', () => {
|
|||||||
headers: restKeyHeaders,
|
headers: restKeyHeaders,
|
||||||
}, (error, response, body) => {
|
}, (error, response, body) => {
|
||||||
expect(response.statusCode).toEqual(401);
|
expect(response.statusCode).toEqual(401);
|
||||||
expect(body.error).toEqual('unauthorized');
|
expect(body.error).toEqual('master key not specified');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -318,4 +318,319 @@ describe('schemas', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('requires the master key to modify schemas', done => {
|
||||||
|
request.post({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {},
|
||||||
|
}, (error, response, body) => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: noAuthHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {},
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(response.statusCode).toEqual(403);
|
||||||
|
expect(body.error).toEqual('unauthorized');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects class name mis-matches in put', done => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {className: 'WrongClassName'}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(response.statusCode).toEqual(400);
|
||||||
|
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||||
|
expect(body.error).toEqual('class name mismatch between WrongClassName and NewClass');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to add fields to non-existent classes', done => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NoClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
newField: {type: 'String'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(response.statusCode).toEqual(400);
|
||||||
|
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||||
|
expect(body.error).toEqual('class NoClass does not exist');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to put to existing fields, even if it would not be a change', done => {
|
||||||
|
var obj = hasAllPODobject();
|
||||||
|
obj.save()
|
||||||
|
.then(() => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
aString: {type: 'String'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(response.statusCode).toEqual(400);
|
||||||
|
expect(body.code).toEqual(255);
|
||||||
|
expect(body.error).toEqual('field aString exists, cannot update');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to delete non-existant fields', done => {
|
||||||
|
var obj = hasAllPODobject();
|
||||||
|
obj.save()
|
||||||
|
.then(() => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
nonExistantKey: {__op: "Delete"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(response.statusCode).toEqual(400);
|
||||||
|
expect(body.code).toEqual(255);
|
||||||
|
expect(body.error).toEqual('field nonExistantKey does not exist, cannot delete');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to add a geopoint to a class that already has one', done => {
|
||||||
|
var obj = hasAllPODobject();
|
||||||
|
obj.save()
|
||||||
|
.then(() => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
newGeo: {type: 'GeoPoint'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(response.statusCode).toEqual(400);
|
||||||
|
expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||||
|
expect(body.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding newGeo when aGeoPoint already exists.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to add two geopoints', done => {
|
||||||
|
var obj = new Parse.Object('NewClass');
|
||||||
|
obj.set('aString', 'aString');
|
||||||
|
obj.save()
|
||||||
|
.then(() => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
newGeo1: {type: 'GeoPoint'},
|
||||||
|
newGeo2: {type: 'GeoPoint'},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(response.statusCode).toEqual(400);
|
||||||
|
expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||||
|
expect(body.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding newGeo2 when newGeo1 already exists.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows you to delete and add a geopoint in the same request', done => {
|
||||||
|
var obj = new Parse.Object('NewClass');
|
||||||
|
obj.set('geo1', new Parse.GeoPoint({latitude: 0, longitude: 0}));
|
||||||
|
obj.save()
|
||||||
|
.then(() => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
geo2: {type: 'GeoPoint'},
|
||||||
|
geo1: {__op: 'Delete'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(dd(body, {
|
||||||
|
"className": "NewClass",
|
||||||
|
"fields": {
|
||||||
|
"ACL": {"type": "ACL"},
|
||||||
|
"createdAt": {"type": "Date"},
|
||||||
|
"objectId": {"type": "String"},
|
||||||
|
"updatedAt": {"type": "Date"},
|
||||||
|
"geo2": {"type": "GeoPoint"},
|
||||||
|
}
|
||||||
|
})).toEqual(undefined);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('put with no modifications returns all fields', done => {
|
||||||
|
var obj = hasAllPODobject();
|
||||||
|
obj.save()
|
||||||
|
.then(() => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {},
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual(plainOldDataSchema);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets you add fields', done => {
|
||||||
|
request.post({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {},
|
||||||
|
}, (error, response, body) => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
newField: {type: 'String'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(dd(body, {
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
"ACL": {"type": "ACL"},
|
||||||
|
"createdAt": {"type": "Date"},
|
||||||
|
"objectId": {"type": "String"},
|
||||||
|
"updatedAt": {"type": "Date"},
|
||||||
|
"newField": {"type": "String"},
|
||||||
|
},
|
||||||
|
})).toEqual(undefined);
|
||||||
|
request.get({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual({
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
newField: {type: 'String'},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets you delete multiple fields and add fields', done => {
|
||||||
|
var obj1 = hasAllPODobject();
|
||||||
|
obj1.save()
|
||||||
|
.then(() => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
aString: {__op: 'Delete'},
|
||||||
|
aNumber: {__op: 'Delete'},
|
||||||
|
aNewString: {type: 'String'},
|
||||||
|
aNewNumber: {type: 'Number'},
|
||||||
|
aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'},
|
||||||
|
aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual({
|
||||||
|
className: 'HasAllPOD',
|
||||||
|
fields: {
|
||||||
|
//Default fields
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
//Custom fields
|
||||||
|
aBool: {type: 'Boolean'},
|
||||||
|
aDate: {type: 'Date'},
|
||||||
|
aObject: {type: 'Object'},
|
||||||
|
aArray: {type: 'Array'},
|
||||||
|
aGeoPoint: {type: 'GeoPoint'},
|
||||||
|
aFile: {type: 'File'},
|
||||||
|
aNewNumber: {type: 'Number'},
|
||||||
|
aNewString: {type: 'String'},
|
||||||
|
aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'},
|
||||||
|
aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var obj2 = new Parse.Object('HasAllPOD');
|
||||||
|
obj2.set('aNewPointer', obj1);
|
||||||
|
var relation = obj2.relation('aNewRelation');
|
||||||
|
relation.add(obj1);
|
||||||
|
obj2.save().then(done); //Just need to make sure saving works on the new object.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will not delete any fields if the additions are invalid', done => {
|
||||||
|
var obj = hasAllPODobject();
|
||||||
|
obj.save()
|
||||||
|
.then(() => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
fakeNewField: {type: 'fake type'},
|
||||||
|
aString: {__op: 'Delete'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||||
|
expect(body.error).toEqual('invalid field type: fake type');
|
||||||
|
request.get({
|
||||||
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(response.body).toEqual(plainOldDataSchema);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export class S3Adapter extends FilesAdapter {
|
|||||||
// The location is the direct S3 link if the option is set, otherwise we serve the file through parse-server
|
// The location is the direct S3 link if the option is set, otherwise we serve the file through parse-server
|
||||||
getFileLocation(config, filename) {
|
getFileLocation(config, filename) {
|
||||||
if (this._directAccess) {
|
if (this._directAccess) {
|
||||||
return ('https://' + this.bucket + '._s3Client.amazonaws.com' + '/' + this._bucketPrefix + filename);
|
return `https://${this._bucket}.s3.amazonaws.com/${this._bucketPrefix + filename}`;
|
||||||
}
|
}
|
||||||
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
|
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Wrapper around Winston logging library with custom query
|
// Wrapper around Winston logging library with custom query
|
||||||
//
|
//
|
||||||
// expected log entry to be in the shape of:
|
// expected log entry to be in the shape of:
|
||||||
// {"level":"info","message":"{ '0': 'Your Message' }","timestamp":"2016-02-04T05:59:27.412Z"}
|
// {"level":"info","message":"Your Message","timestamp":"2016-02-04T05:59:27.412Z"}
|
||||||
//
|
//
|
||||||
import { LoggerAdapter } from './LoggerAdapter';
|
import { LoggerAdapter } from './LoggerAdapter';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
|
|||||||
230
src/Adapters/Push/OneSignalPushAdapter.js
Normal file
230
src/Adapters/Push/OneSignalPushAdapter.js
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
"use strict";
|
||||||
|
// ParsePushAdapter is the default implementation of
|
||||||
|
// PushAdapter, it uses GCM for android push and APNS
|
||||||
|
// for ios push.
|
||||||
|
|
||||||
|
const Parse = require('parse/node').Parse;
|
||||||
|
var deepcopy = require('deepcopy');
|
||||||
|
|
||||||
|
function OneSignalPushAdapter(pushConfig) {
|
||||||
|
this.https = require('https');
|
||||||
|
|
||||||
|
this.validPushTypes = ['ios', 'android'];
|
||||||
|
this.senderMap = {};
|
||||||
|
|
||||||
|
pushConfig = pushConfig || {};
|
||||||
|
this.OneSignalConfig = {};
|
||||||
|
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
|
||||||
|
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
|
||||||
|
|
||||||
|
this.senderMap['ios'] = this.sendToAPNS.bind(this);
|
||||||
|
this.senderMap['android'] = this.sendToGCM.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of valid push types.
|
||||||
|
* @returns {Array} An array of valid push types
|
||||||
|
*/
|
||||||
|
OneSignalPushAdapter.prototype.getValidPushTypes = function() {
|
||||||
|
return this.validPushTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
OneSignalPushAdapter.prototype.send = function(data, installations) {
|
||||||
|
console.log("Sending notification to "+installations.length+" devices.")
|
||||||
|
let deviceMap = classifyInstallation(installations, this.validPushTypes);
|
||||||
|
|
||||||
|
let sendPromises = [];
|
||||||
|
for (let pushType in deviceMap) {
|
||||||
|
let sender = this.senderMap[pushType];
|
||||||
|
if (!sender) {
|
||||||
|
console.log('Can not find sender for push type %s, %j', pushType, data);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let devices = deviceMap[pushType];
|
||||||
|
|
||||||
|
if(devices.length > 0) {
|
||||||
|
sendPromises.push(sender(data, devices));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Parse.Promise.when(sendPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
OneSignalPushAdapter.prototype.sendToAPNS = function(data,tokens) {
|
||||||
|
|
||||||
|
data= deepcopy(data['data']);
|
||||||
|
|
||||||
|
var post = {};
|
||||||
|
if(data['badge']) {
|
||||||
|
if(data['badge'] == "Increment") {
|
||||||
|
post['ios_badgeType'] = 'Increase';
|
||||||
|
post['ios_badgeCount'] = 1;
|
||||||
|
} else {
|
||||||
|
post['ios_badgeType'] = 'SetTo';
|
||||||
|
post['ios_badgeCount'] = data['badge'];
|
||||||
|
}
|
||||||
|
delete data['badge'];
|
||||||
|
}
|
||||||
|
if(data['alert']) {
|
||||||
|
post['contents'] = {en: data['alert']};
|
||||||
|
delete data['alert'];
|
||||||
|
}
|
||||||
|
if(data['sound']) {
|
||||||
|
post['ios_sound'] = data['sound'];
|
||||||
|
delete data['sound'];
|
||||||
|
}
|
||||||
|
if(data['content-available'] == 1) {
|
||||||
|
post['content_available'] = true;
|
||||||
|
delete data['content-available'];
|
||||||
|
}
|
||||||
|
post['data'] = data;
|
||||||
|
|
||||||
|
let promise = new Parse.Promise();
|
||||||
|
|
||||||
|
var chunk = 2000 // OneSignal can process 2000 devices at a time
|
||||||
|
var tokenlength=tokens.length;
|
||||||
|
var offset = 0
|
||||||
|
// handle onesignal response. Start next batch if there's not an error.
|
||||||
|
let handleResponse = function(wasSuccessful) {
|
||||||
|
if (!wasSuccessful) {
|
||||||
|
return promise.reject("OneSignal Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(offset >= tokenlength) {
|
||||||
|
promise.resolve()
|
||||||
|
} else {
|
||||||
|
this.sendNext();
|
||||||
|
}
|
||||||
|
}.bind(this)
|
||||||
|
|
||||||
|
this.sendNext = function() {
|
||||||
|
post['include_ios_tokens'] = [];
|
||||||
|
tokens.slice(offset,offset+chunk).forEach(function(i) {
|
||||||
|
post['include_ios_tokens'].push(i['deviceToken'])
|
||||||
|
})
|
||||||
|
offset+=chunk;
|
||||||
|
this.sendToOneSignal(post, handleResponse);
|
||||||
|
}.bind(this)
|
||||||
|
|
||||||
|
this.sendNext()
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
OneSignalPushAdapter.prototype.sendToGCM = function(data,tokens) {
|
||||||
|
data= deepcopy(data['data']);
|
||||||
|
|
||||||
|
var post = {};
|
||||||
|
|
||||||
|
if(data['alert']) {
|
||||||
|
post['contents'] = {en: data['alert']};
|
||||||
|
delete data['alert'];
|
||||||
|
}
|
||||||
|
if(data['title']) {
|
||||||
|
post['title'] = {en: data['title']};
|
||||||
|
delete data['title'];
|
||||||
|
}
|
||||||
|
if(data['uri']) {
|
||||||
|
post['url'] = data['uri'];
|
||||||
|
}
|
||||||
|
|
||||||
|
post['data'] = data;
|
||||||
|
|
||||||
|
let promise = new Parse.Promise();
|
||||||
|
|
||||||
|
var chunk = 2000 // OneSignal can process 2000 devices at a time
|
||||||
|
var tokenlength=tokens.length;
|
||||||
|
var offset = 0
|
||||||
|
// handle onesignal response. Start next batch if there's not an error.
|
||||||
|
let handleResponse = function(wasSuccessful) {
|
||||||
|
if (!wasSuccessful) {
|
||||||
|
return promise.reject("OneSIgnal Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(offset >= tokenlength) {
|
||||||
|
promise.resolve()
|
||||||
|
} else {
|
||||||
|
this.sendNext();
|
||||||
|
}
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
this.sendNext = function() {
|
||||||
|
post['include_android_reg_ids'] = [];
|
||||||
|
tokens.slice(offset,offset+chunk).forEach(function(i) {
|
||||||
|
post['include_android_reg_ids'].push(i['deviceToken'])
|
||||||
|
})
|
||||||
|
offset+=chunk;
|
||||||
|
this.sendToOneSignal(post, handleResponse);
|
||||||
|
}.bind(this)
|
||||||
|
|
||||||
|
|
||||||
|
this.sendNext();
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OneSignalPushAdapter.prototype.sendToOneSignal = function(data, cb) {
|
||||||
|
let headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Basic "+this.OneSignalConfig['apiKey']
|
||||||
|
};
|
||||||
|
let options = {
|
||||||
|
host: "onesignal.com",
|
||||||
|
port: 443,
|
||||||
|
path: "/api/v1/notifications",
|
||||||
|
method: "POST",
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
data['app_id'] = this.OneSignalConfig['appId'];
|
||||||
|
|
||||||
|
let request = this.https.request(options, function(res) {
|
||||||
|
if(res.statusCode < 299) {
|
||||||
|
cb(true);
|
||||||
|
} else {
|
||||||
|
console.log('OneSignal Error');
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
console.log(chunk.toString())
|
||||||
|
});
|
||||||
|
cb(false)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
request.on('error', function(e) {
|
||||||
|
console.log("Error connecting to OneSignal")
|
||||||
|
console.log(e);
|
||||||
|
cb(false);
|
||||||
|
});
|
||||||
|
request.write(JSON.stringify(data))
|
||||||
|
request.end();
|
||||||
|
}
|
||||||
|
/**g
|
||||||
|
* Classify the device token of installations based on its device type.
|
||||||
|
* @param {Object} installations An array of installations
|
||||||
|
* @param {Array} validPushTypes An array of valid push types(string)
|
||||||
|
* @returns {Object} A map whose key is device type and value is an array of device
|
||||||
|
*/
|
||||||
|
function classifyInstallation(installations, validPushTypes) {
|
||||||
|
// Init deviceTokenMap, create a empty array for each valid pushType
|
||||||
|
let deviceMap = {};
|
||||||
|
for (let validPushType of validPushTypes) {
|
||||||
|
deviceMap[validPushType] = [];
|
||||||
|
}
|
||||||
|
for (let installation of installations) {
|
||||||
|
// No deviceToken, ignore
|
||||||
|
if (!installation.deviceToken) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let pushType = installation.deviceType;
|
||||||
|
if (deviceMap[pushType]) {
|
||||||
|
deviceMap[pushType].push({
|
||||||
|
deviceToken: installation.deviceToken
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Unknown push type from installation %j', installation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deviceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||||
|
OneSignalPushAdapter.classifyInstallation = classifyInstallation;
|
||||||
|
}
|
||||||
|
module.exports = OneSignalPushAdapter;
|
||||||
@@ -20,10 +20,12 @@ function Config(applicationId, mount) {
|
|||||||
this.restAPIKey = cacheInfo.restAPIKey;
|
this.restAPIKey = cacheInfo.restAPIKey;
|
||||||
this.fileKey = cacheInfo.fileKey;
|
this.fileKey = cacheInfo.fileKey;
|
||||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||||
|
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
|
||||||
|
|
||||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
||||||
this.filesController = cacheInfo.filesController;
|
this.filesController = cacheInfo.filesController;
|
||||||
|
|
||||||
|
this.oauth = cacheInfo.oauth;
|
||||||
this.mount = mount;
|
this.mount = mount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -377,7 +377,11 @@ RestQuery.prototype.handleInclude = function() {
|
|||||||
this.include = this.include.slice(1);
|
this.include = this.include.slice(1);
|
||||||
return this.handleInclude();
|
return this.handleInclude();
|
||||||
});
|
});
|
||||||
|
} else if (this.include.length > 0) {
|
||||||
|
this.include = this.include.slice(1);
|
||||||
|
return this.handleInclude();
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathResponse;
|
return pathResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -415,6 +419,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 = {
|
||||||
|
|||||||
115
src/RestWrite.js
115
src/RestWrite.js
@@ -9,7 +9,7 @@ var cache = require('./cache');
|
|||||||
var Config = require('./Config');
|
var Config = require('./Config');
|
||||||
var cryptoUtils = require('./cryptoUtils');
|
var cryptoUtils = require('./cryptoUtils');
|
||||||
var passwordCrypto = require('./password');
|
var passwordCrypto = require('./password');
|
||||||
var facebook = require('./facebook');
|
var oauth = require("./oauth");
|
||||||
var Parse = require('parse/node');
|
var Parse = require('parse/node');
|
||||||
var triggers = require('./triggers');
|
var triggers = require('./triggers');
|
||||||
|
|
||||||
@@ -147,19 +147,26 @@ RestWrite.prototype.validateAuthData = function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var facebookData = this.data.authData.facebook;
|
var authData = this.data.authData;
|
||||||
var anonData = this.data.authData.anonymous;
|
var anonData = this.data.authData.anonymous;
|
||||||
|
|
||||||
if (anonData === null ||
|
if (this.config.enableAnonymousUsers === true && (anonData === null ||
|
||||||
(anonData && anonData.id)) {
|
(anonData && anonData.id))) {
|
||||||
return this.handleAnonymousAuthData();
|
return this.handleAnonymousAuthData();
|
||||||
} else if (facebookData === null ||
|
}
|
||||||
(facebookData && facebookData.id && facebookData.access_token)) {
|
|
||||||
return this.handleFacebookAuthData();
|
// Not anon, try other providers
|
||||||
} else {
|
var providers = Object.keys(authData);
|
||||||
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
|
if (!anonData && providers.length == 1) {
|
||||||
'This authentication method is unsupported.');
|
var provider = providers[0];
|
||||||
|
var providerAuthData = authData[provider];
|
||||||
|
var hasToken = (providerAuthData && providerAuthData.id);
|
||||||
|
if (providerAuthData === null || hasToken) {
|
||||||
|
return this.handleOAuthAuthData(provider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
|
||||||
|
'This authentication method is unsupported.');
|
||||||
};
|
};
|
||||||
|
|
||||||
RestWrite.prototype.handleAnonymousAuthData = function() {
|
RestWrite.prototype.handleAnonymousAuthData = function() {
|
||||||
@@ -208,27 +215,71 @@ RestWrite.prototype.handleAnonymousAuthData = function() {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RestWrite.prototype.handleFacebookAuthData = function() {
|
RestWrite.prototype.handleOAuthAuthData = function(provider) {
|
||||||
var facebookData = this.data.authData.facebook;
|
var authData = this.data.authData[provider];
|
||||||
if (facebookData === null && this.query) {
|
|
||||||
// We are unlinking from Facebook.
|
if (authData === null && this.query) {
|
||||||
this.data._auth_data_facebook = null;
|
// We are unlinking from the provider.
|
||||||
|
this.data["_auth_data_" + provider ] = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return facebook.validateUserId(facebookData.id,
|
var appIds;
|
||||||
facebookData.access_token)
|
var oauthOptions = this.config.oauth[provider];
|
||||||
|
if (oauthOptions) {
|
||||||
|
appIds = oauthOptions.appIds;
|
||||||
|
} else if (provider == "facebook") {
|
||||||
|
appIds = this.config.facebookAppIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validateAuthData;
|
||||||
|
var validateAppId;
|
||||||
|
|
||||||
|
|
||||||
|
if (oauth[provider]) {
|
||||||
|
validateAuthData = oauth[provider].validateAuthData;
|
||||||
|
validateAppId = oauth[provider].validateAppId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try the configuration methods
|
||||||
|
if (oauthOptions) {
|
||||||
|
if (oauthOptions.module) {
|
||||||
|
validateAuthData = require(oauthOptions.module).validateAuthData;
|
||||||
|
validateAppId = require(oauthOptions.module).validateAppId;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (oauthOptions.validateAuthData) {
|
||||||
|
validateAuthData = oauthOptions.validateAuthData;
|
||||||
|
}
|
||||||
|
if (oauthOptions.validateAppId) {
|
||||||
|
validateAppId = oauthOptions.validateAppId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// try the custom provider first, fallback on the oauth implementation
|
||||||
|
|
||||||
|
if (!validateAuthData || !validateAppId) {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return validateAuthData(authData, oauthOptions)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return facebook.validateAppId(this.config.facebookAppIds,
|
if (appIds && typeof validateAppId === "function") {
|
||||||
facebookData.access_token);
|
return validateAppId(appIds, authData, oauthOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No validation required by the developer
|
||||||
|
return Promise.resolve();
|
||||||
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// Check if this user already exists
|
// Check if this user already exists
|
||||||
// TODO: does this handle re-linking correctly?
|
// TODO: does this handle re-linking correctly?
|
||||||
|
var query = {};
|
||||||
|
query['authData.' + provider + '.id'] = authData.id;
|
||||||
return this.config.database.find(
|
return this.config.database.find(
|
||||||
this.className,
|
this.className,
|
||||||
{'authData.facebook.id': facebookData.id}, {});
|
query, {});
|
||||||
}).then((results) => {
|
}).then((results) => {
|
||||||
this.storage['authProvider'] = "facebook";
|
this.storage['authProvider'] = provider;
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
if (!this.query) {
|
if (!this.query) {
|
||||||
// We're signing up, but this user already exists. Short-circuit
|
// We're signing up, but this user already exists. Short-circuit
|
||||||
@@ -247,7 +298,7 @@ RestWrite.prototype.handleFacebookAuthData = function() {
|
|||||||
delete this.data.authData;
|
delete this.data.authData;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// We're trying to create a duplicate FB auth. Forbid it
|
// We're trying to create a duplicate oauth auth. Forbid it
|
||||||
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 {
|
||||||
@@ -256,12 +307,12 @@ RestWrite.prototype.handleFacebookAuthData = function() {
|
|||||||
|
|
||||||
// This FB auth does not already exist, so transform it to a
|
// This FB auth does not already exist, so transform it to a
|
||||||
// saveable format
|
// saveable format
|
||||||
this.data._auth_data_facebook = facebookData;
|
this.data["_auth_data_" + provider ] = authData;
|
||||||
|
|
||||||
// Delete the rest format key before saving
|
// Delete the rest format key before saving
|
||||||
delete this.data.authData;
|
delete this.data.authData;
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
// The non-third-party parts of User transformation
|
// The non-third-party parts of User transformation
|
||||||
RestWrite.prototype.transformUser = function() {
|
RestWrite.prototype.transformUser = function() {
|
||||||
@@ -306,7 +357,7 @@ RestWrite.prototype.transformUser = function() {
|
|||||||
if (!this.data.password) {
|
if (!this.data.password) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.query) {
|
if (this.query && !this.auth.isMaster ) {
|
||||||
this.storage['clearSessions'] = true;
|
this.storage['clearSessions'] = true;
|
||||||
}
|
}
|
||||||
return passwordCrypto.hash(this.data.password).then((hashedPassword) => {
|
return passwordCrypto.hash(this.data.password).then((hashedPassword) => {
|
||||||
@@ -485,11 +536,6 @@ RestWrite.prototype.handleInstallation = function() {
|
|||||||
this.data.installationId = this.data.installationId.toLowerCase();
|
this.data.installationId = this.data.installationId.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.data.deviceToken && this.data.deviceType == 'android') {
|
|
||||||
throw new Parse.Error(114,
|
|
||||||
'deviceToken may not be set for deviceType android');
|
|
||||||
}
|
|
||||||
|
|
||||||
var promise = Promise.resolve();
|
var promise = Promise.resolve();
|
||||||
|
|
||||||
if (this.query && this.query.objectId) {
|
if (this.query && this.query.objectId) {
|
||||||
@@ -660,6 +706,13 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
|||||||
this.response.updatedAt = this.updatedAt;
|
this.response.updatedAt = this.updatedAt;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// Set the default ACL for the new _User
|
||||||
|
if (!this.data.ACL && this.className === '_User') {
|
||||||
|
var ACL = {};
|
||||||
|
ACL[this.data.objectId] = { read: true, write: true };
|
||||||
|
ACL['*'] = { read: true, write: false };
|
||||||
|
this.data.ACL = ACL;
|
||||||
|
}
|
||||||
// Run a create
|
// Run a create
|
||||||
return this.config.database.create(this.className, this.data, options)
|
return this.config.database.create(this.className, this.data, options)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export class ClassesRouter {
|
|||||||
}
|
}
|
||||||
if (body.limit) {
|
if (body.limit) {
|
||||||
options.limit = Number(body.limit);
|
options.limit = Number(body.limit);
|
||||||
|
} else {
|
||||||
|
options.limit = Number(100);
|
||||||
}
|
}
|
||||||
if (body.order) {
|
if (body.order) {
|
||||||
options.order = String(body.order);
|
options.order = String(body.order);
|
||||||
@@ -51,6 +53,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] };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
|
|
||||||
handleCreate(req) {
|
handleCreate(req) {
|
||||||
let data = deepcopy(req.body);
|
let data = deepcopy(req.body);
|
||||||
data.installationId = req.info.installationId;
|
|
||||||
req.body = data;
|
req.body = data;
|
||||||
req.params.className = '_User';
|
req.params.className = '_User';
|
||||||
return super.handleCreate(req);
|
return super.handleCreate(req);
|
||||||
@@ -41,8 +40,7 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
|
|
||||||
handleMe(req) {
|
handleMe(req) {
|
||||||
if (!req.info || !req.info.sessionToken) {
|
if (!req.info || !req.info.sessionToken) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid session token');
|
||||||
'Object not found.');
|
|
||||||
}
|
}
|
||||||
return rest.find(req.config, Auth.master(req.config), '_Session',
|
return rest.find(req.config, Auth.master(req.config), '_Session',
|
||||||
{ _session_token: req.info.sessionToken },
|
{ _session_token: req.info.sessionToken },
|
||||||
@@ -51,8 +49,7 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
if (!response.results ||
|
if (!response.results ||
|
||||||
response.results.length == 0 ||
|
response.results.length == 0 ||
|
||||||
!response.results[0].user) {
|
!response.results[0].user) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid session token');
|
||||||
'Object not found.');
|
|
||||||
} else {
|
} else {
|
||||||
let user = response.results[0].user;
|
let user = response.results[0].user;
|
||||||
return { response: user };
|
return { response: user };
|
||||||
@@ -145,10 +142,10 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
let router = new PromiseRouter();
|
let router = new PromiseRouter();
|
||||||
router.route('GET', '/users', req => { return this.handleFind(req); });
|
router.route('GET', '/users', req => { return this.handleFind(req); });
|
||||||
router.route('POST', '/users', req => { return this.handleCreate(req); });
|
router.route('POST', '/users', req => { return this.handleCreate(req); });
|
||||||
|
router.route('GET', '/users/me', req => { return this.handleMe(req); });
|
||||||
router.route('GET', '/users/:objectId', req => { return this.handleGet(req); });
|
router.route('GET', '/users/:objectId', req => { return this.handleGet(req); });
|
||||||
router.route('PUT', '/users/:objectId', req => { return this.handleUpdate(req); });
|
router.route('PUT', '/users/:objectId', req => { return this.handleUpdate(req); });
|
||||||
router.route('DELETE', '/users/:objectId', req => { return this.handleDelete(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('GET', '/login', req => { return this.handleLogIn(req); });
|
||||||
router.route('POST', '/logout', req => { return this.handleLogOut(req); });
|
router.route('POST', '/logout', req => { return this.handleLogOut(req); });
|
||||||
router.route('POST', '/requestPasswordReset', () => {
|
router.route('POST', '/requestPasswordReset', () => {
|
||||||
|
|||||||
169
src/Schema.js
169
src/Schema.js
@@ -116,7 +116,7 @@ function schemaAPITypeToMongoFieldType(type) {
|
|||||||
return invalidJsonError;
|
return invalidJsonError;
|
||||||
} else if (!classNameIsValid(type.targetClass)) {
|
} else if (!classNameIsValid(type.targetClass)) {
|
||||||
return { error: invalidClassNameMessage(type.targetClass), code: Parse.Error.INVALID_CLASS_NAME };
|
return { error: invalidClassNameMessage(type.targetClass), code: Parse.Error.INVALID_CLASS_NAME };
|
||||||
} else {
|
} else {
|
||||||
return { result: '*' + type.targetClass };
|
return { result: '*' + type.targetClass };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,6 +200,114 @@ Schema.prototype.reload = function() {
|
|||||||
return load(this.collection);
|
return load(this.collection);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Returns { code, error } if invalid, or { result }, an object
|
||||||
|
// suitable for inserting into _SCHEMA collection, otherwise
|
||||||
|
function mongoSchemaFromFieldsAndClassName(fields, className) {
|
||||||
|
if (!classNameIsValid(className)) {
|
||||||
|
return {
|
||||||
|
code: Parse.Error.INVALID_CLASS_NAME,
|
||||||
|
error: invalidClassNameMessage(className),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var fieldName in fields) {
|
||||||
|
if (!fieldNameIsValid(fieldName)) {
|
||||||
|
return {
|
||||||
|
code: Parse.Error.INVALID_KEY_NAME,
|
||||||
|
error: 'invalid field name: ' + fieldName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!fieldNameIsValidForClass(fieldName, className)) {
|
||||||
|
return {
|
||||||
|
code: 136,
|
||||||
|
error: 'field ' + fieldName + ' cannot be added',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mongoObject = {
|
||||||
|
_id: className,
|
||||||
|
objectId: 'string',
|
||||||
|
updatedAt: 'string',
|
||||||
|
createdAt: 'string'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var fieldName in defaultColumns[className]) {
|
||||||
|
var validatedField = schemaAPITypeToMongoFieldType(defaultColumns[className][fieldName]);
|
||||||
|
if (!validatedField.result) {
|
||||||
|
return validatedField;
|
||||||
|
}
|
||||||
|
mongoObject[fieldName] = validatedField.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var fieldName in fields) {
|
||||||
|
var validatedField = schemaAPITypeToMongoFieldType(fields[fieldName]);
|
||||||
|
if (!validatedField.result) {
|
||||||
|
return validatedField;
|
||||||
|
}
|
||||||
|
mongoObject[fieldName] = validatedField.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint');
|
||||||
|
if (geoPoints.length > 1) {
|
||||||
|
return {
|
||||||
|
code: Parse.Error.INCORRECT_TYPE,
|
||||||
|
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { result: mongoObject };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mongoFieldTypeToSchemaAPIType(type) {
|
||||||
|
if (type[0] === '*') {
|
||||||
|
return {
|
||||||
|
type: 'Pointer',
|
||||||
|
targetClass: type.slice(1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type.startsWith('relation<')) {
|
||||||
|
return {
|
||||||
|
type: 'Relation',
|
||||||
|
targetClass: type.slice('relation<'.length, type.length - 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'number': return {type: 'Number'};
|
||||||
|
case 'string': return {type: 'String'};
|
||||||
|
case 'boolean': return {type: 'Boolean'};
|
||||||
|
case 'date': return {type: 'Date'};
|
||||||
|
case 'map':
|
||||||
|
case 'object': return {type: 'Object'};
|
||||||
|
case 'array': return {type: 'Array'};
|
||||||
|
case 'geopoint': return {type: 'GeoPoint'};
|
||||||
|
case 'file': return {type: 'File'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a new schema (in schema API response format) out of an
|
||||||
|
// existing mongo schema + a schemas API put request. This response
|
||||||
|
// does not include the default fields, as it is intended to be passed
|
||||||
|
// to mongoSchemaFromFieldsAndClassName. No validation is done here, it
|
||||||
|
// is done in mongoSchemaFromFieldsAndClassName.
|
||||||
|
function buildMergedSchemaObject(mongoObject, putRequest) {
|
||||||
|
var newSchema = {};
|
||||||
|
for (var oldField in mongoObject) {
|
||||||
|
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
|
||||||
|
var fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'
|
||||||
|
if (!fieldIsDeleted) {
|
||||||
|
newSchema[oldField] = mongoFieldTypeToSchemaAPIType(mongoObject[oldField]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var newField in putRequest) {
|
||||||
|
if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') {
|
||||||
|
newSchema[newField] = putRequest[newField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSchema;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new class that includes the three default fields.
|
// Create a new class that includes the three default fields.
|
||||||
// ACL is an implicit column that does not get an entry in the
|
// ACL is an implicit column that does not get an entry in the
|
||||||
// _SCHEMAS database. Returns a promise that resolves with the
|
// _SCHEMAS database. Returns a promise that resolves with the
|
||||||
@@ -215,58 +323,13 @@ Schema.prototype.addClassIfNotExists = function(className, fields) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!classNameIsValid(className)) {
|
var mongoObject = mongoSchemaFromFieldsAndClassName(fields, className);
|
||||||
return Promise.reject({
|
|
||||||
code: Parse.Error.INVALID_CLASS_NAME,
|
if (!mongoObject.result) {
|
||||||
error: invalidClassNameMessage(className),
|
return Promise.reject(mongoObject);
|
||||||
});
|
|
||||||
}
|
|
||||||
for (var fieldName in fields) {
|
|
||||||
if (!fieldNameIsValid(fieldName)) {
|
|
||||||
return Promise.reject({
|
|
||||||
code: Parse.Error.INVALID_KEY_NAME,
|
|
||||||
error: 'invalid field name: ' + fieldName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!fieldNameIsValidForClass(fieldName, className)) {
|
|
||||||
return Promise.reject({
|
|
||||||
code: 136,
|
|
||||||
error: 'field ' + fieldName + ' cannot be added',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var mongoObject = {
|
return this.collection.insertOne(mongoObject.result)
|
||||||
_id: className,
|
|
||||||
objectId: 'string',
|
|
||||||
updatedAt: 'string',
|
|
||||||
createdAt: 'string'
|
|
||||||
};
|
|
||||||
for (var fieldName in defaultColumns[className]) {
|
|
||||||
var validatedField = schemaAPITypeToMongoFieldType(defaultColumns[className][fieldName]);
|
|
||||||
if (validatedField.code) {
|
|
||||||
return Promise.reject(validatedField);
|
|
||||||
}
|
|
||||||
mongoObject[fieldName] = validatedField.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var fieldName in fields) {
|
|
||||||
var validatedField = schemaAPITypeToMongoFieldType(fields[fieldName]);
|
|
||||||
if (validatedField.code) {
|
|
||||||
return Promise.reject(validatedField);
|
|
||||||
}
|
|
||||||
mongoObject[fieldName] = validatedField.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint');
|
|
||||||
if (geoPoints.length > 1) {
|
|
||||||
return Promise.reject({
|
|
||||||
code: Parse.Error.INCORRECT_TYPE,
|
|
||||||
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.collection.insertOne(mongoObject)
|
|
||||||
.then(result => result.ops[0])
|
.then(result => result.ops[0])
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error.code === 11000) { //Mongo's duplicate key error
|
if (error.code === 11000) { //Mongo's duplicate key error
|
||||||
@@ -651,4 +714,8 @@ function getObjectType(obj) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
load: load,
|
load: load,
|
||||||
classNameIsValid: classNameIsValid,
|
classNameIsValid: classNameIsValid,
|
||||||
|
mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName,
|
||||||
|
schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType,
|
||||||
|
buildMergedSchemaObject: buildMergedSchemaObject,
|
||||||
|
mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ var https = require('https');
|
|||||||
var Parse = require('parse/node').Parse;
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
function validateUserId(userId, access_token) {
|
function validateAuthData(authData) {
|
||||||
return graphRequest('me?fields=id&access_token=' + access_token)
|
return graphRequest('me?fields=id&access_token=' + authData.access_token)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data && data.id == userId) {
|
if (data && data.id == authData.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
@@ -16,7 +16,8 @@ function validateUserId(userId, access_token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this app id is valid.
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
function validateAppId(appIds, access_token) {
|
function validateAppId(appIds, authData) {
|
||||||
|
var access_token = authData.access_token;
|
||||||
if (!appIds.length) {
|
if (!appIds.length) {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
@@ -53,5 +54,5 @@ function graphRequest(path) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
validateAppId: validateAppId,
|
validateAppId: validateAppId,
|
||||||
validateUserId: validateUserId
|
validateAuthData: validateAuthData
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ var router = new PromiseRouter();
|
|||||||
|
|
||||||
function handleCloudFunction(req) {
|
function handleCloudFunction(req) {
|
||||||
if (Parse.Cloud.Functions[req.params.functionName]) {
|
if (Parse.Cloud.Functions[req.params.functionName]) {
|
||||||
|
|
||||||
|
const params = Object.assign({}, req.body, req.query);
|
||||||
|
|
||||||
if (Parse.Cloud.Validators[req.params.functionName]) {
|
if (Parse.Cloud.Validators[req.params.functionName]) {
|
||||||
var result = Parse.Cloud.Validators[req.params.functionName](req.body || {});
|
var result = Parse.Cloud.Validators[req.params.functionName](params);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Validation failed.');
|
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Validation failed.');
|
||||||
}
|
}
|
||||||
@@ -19,7 +22,7 @@ function handleCloudFunction(req) {
|
|||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
var response = createResponseObject(resolve, reject);
|
var response = createResponseObject(resolve, reject);
|
||||||
var request = {
|
var request = {
|
||||||
params: req.body || {},
|
params: params,
|
||||||
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
|
installationId: req.info.installationId
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ function ParseServer(args) {
|
|||||||
restAPIKey: args.restAPIKey || '',
|
restAPIKey: args.restAPIKey || '',
|
||||||
fileKey: args.fileKey || 'invalid-file-key',
|
fileKey: args.fileKey || 'invalid-file-key',
|
||||||
facebookAppIds: args.facebookAppIds || [],
|
facebookAppIds: args.facebookAppIds || [],
|
||||||
filesController: filesController
|
filesController: filesController,
|
||||||
|
enableAnonymousUsers: args.enableAnonymousUsers || true,
|
||||||
|
oauth: args.oauth || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// To maintain compatibility. TODO: Remove in v2.1
|
// To maintain compatibility. TODO: Remove in v2.1
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ function handleParseHeaders(req, res, next) {
|
|||||||
restAPIKey: req.get('X-Parse-REST-API-Key')
|
restAPIKey: req.get('X-Parse-REST-API-Key')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (req.body && req.body._noBody) {
|
||||||
|
// Unity SDK sends a _noBody key which needs to be removed.
|
||||||
|
// Unclear at this point if action needs to be taken.
|
||||||
|
delete req.body._noBody;
|
||||||
|
}
|
||||||
|
|
||||||
var fileViaJSON = false;
|
var fileViaJSON = false;
|
||||||
|
|
||||||
if (!info.appId || !cache.apps[info.appId]) {
|
if (!info.appId || !cache.apps[info.appId]) {
|
||||||
|
|||||||
226
src/oauth/OAuth1Client.js
Normal file
226
src/oauth/OAuth1Client.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
var https = require('https'),
|
||||||
|
crypto = require('crypto');
|
||||||
|
|
||||||
|
var OAuth = function(options) {
|
||||||
|
this.consumer_key = options.consumer_key;
|
||||||
|
this.consumer_secret = options.consumer_secret;
|
||||||
|
this.auth_token = options.auth_token;
|
||||||
|
this.auth_token_secret = options.auth_token_secret;
|
||||||
|
this.host = options.host;
|
||||||
|
this.oauth_params = options.oauth_params || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
OAuth.prototype.send = function(method, path, params, body){
|
||||||
|
|
||||||
|
var request = this.buildRequest(method, path, params, body);
|
||||||
|
// Encode the body properly, the current Parse Implementation don't do it properly
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var httpRequest = https.request(request, function(res) {
|
||||||
|
var data = '';
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', function() {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
}).on('error', function(e) {
|
||||||
|
reject('Failed to make an OAuth request');
|
||||||
|
});
|
||||||
|
if (request.body) {
|
||||||
|
httpRequest.write(request.body);
|
||||||
|
}
|
||||||
|
httpRequest.end();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAuth.prototype.buildRequest = function(method, path, params, body) {
|
||||||
|
if (path.indexOf("/") != 0) {
|
||||||
|
path = "/"+path;
|
||||||
|
}
|
||||||
|
if (params && Object.keys(params).length > 0) {
|
||||||
|
path += "?" + OAuth.buildParameterString(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
host: this.host,
|
||||||
|
path: path,
|
||||||
|
method: method.toUpperCase()
|
||||||
|
};
|
||||||
|
|
||||||
|
var oauth_params = this.oauth_params || {};
|
||||||
|
oauth_params.oauth_consumer_key = this.consumer_key;
|
||||||
|
if(this.auth_token){
|
||||||
|
oauth_params["oauth_token"] = this.auth_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
request = OAuth.signRequest(request, oauth_params, this.consumer_secret, this.auth_token_secret);
|
||||||
|
|
||||||
|
if (body && Object.keys(body).length > 0) {
|
||||||
|
request.body = OAuth.buildParameterString(body);
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth.prototype.get = function(path, params) {
|
||||||
|
return this.send("GET", path, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth.prototype.post = function(path, params, body) {
|
||||||
|
return this.send("POST", path, params, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Proper string %escape encoding
|
||||||
|
*/
|
||||||
|
OAuth.encode = function(str) {
|
||||||
|
// discuss at: http://phpjs.org/functions/rawurlencode/
|
||||||
|
// original by: Brett Zamir (http://brett-zamir.me)
|
||||||
|
// input by: travc
|
||||||
|
// input by: Brett Zamir (http://brett-zamir.me)
|
||||||
|
// input by: Michael Grier
|
||||||
|
// input by: Ratheous
|
||||||
|
// bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||||
|
// bugfixed by: Brett Zamir (http://brett-zamir.me)
|
||||||
|
// bugfixed by: Joris
|
||||||
|
// reimplemented by: Brett Zamir (http://brett-zamir.me)
|
||||||
|
// reimplemented by: Brett Zamir (http://brett-zamir.me)
|
||||||
|
// note: This reflects PHP 5.3/6.0+ behavior
|
||||||
|
// note: Please be aware that this function expects to encode into UTF-8 encoded strings, as found on
|
||||||
|
// note: pages served as UTF-8
|
||||||
|
// example 1: rawurlencode('Kevin van Zonneveld!');
|
||||||
|
// returns 1: 'Kevin%20van%20Zonneveld%21'
|
||||||
|
// example 2: rawurlencode('http://kevin.vanzonneveld.net/');
|
||||||
|
// returns 2: 'http%3A%2F%2Fkevin.vanzonneveld.net%2F'
|
||||||
|
// example 3: rawurlencode('http://www.google.nl/search?q=php.js&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a');
|
||||||
|
// returns 3: 'http%3A%2F%2Fwww.google.nl%2Fsearch%3Fq%3Dphp.js%26ie%3Dutf-8%26oe%3Dutf-8%26aq%3Dt%26rls%3Dcom.ubuntu%3Aen-US%3Aunofficial%26client%3Dfirefox-a'
|
||||||
|
|
||||||
|
str = (str + '')
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
// Tilde should be allowed unescaped in future versions of PHP (as reflected below), but if you want to reflect current
|
||||||
|
// PHP behavior, you would need to add ".replace(/~/g, '%7E');" to the following.
|
||||||
|
return encodeURIComponent(str)
|
||||||
|
.replace(/!/g, '%21')
|
||||||
|
.replace(/'/g, '%27')
|
||||||
|
.replace(/\(/g, '%28')
|
||||||
|
.replace(/\)/g, '%29')
|
||||||
|
.replace(/\*/g, '%2A');
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth.signatureMethod = "HMAC-SHA1";
|
||||||
|
OAuth.version = "1.0";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Generate a nonce
|
||||||
|
*/
|
||||||
|
OAuth.nonce = function(){
|
||||||
|
var text = "";
|
||||||
|
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
|
for( var i=0; i < 30; i++ )
|
||||||
|
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth.buildParameterString = function(obj){
|
||||||
|
var result = {};
|
||||||
|
|
||||||
|
// Sort keys and encode values
|
||||||
|
if (obj) {
|
||||||
|
var keys = Object.keys(obj).sort();
|
||||||
|
|
||||||
|
// Map key=value, join them by &
|
||||||
|
return keys.map(function(key){
|
||||||
|
return key + "=" + OAuth.encode(obj[key]);
|
||||||
|
}).join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Build the signature string from the object
|
||||||
|
*/
|
||||||
|
|
||||||
|
OAuth.buildSignatureString = function(method, url, parameters){
|
||||||
|
return [method.toUpperCase(), OAuth.encode(url), OAuth.encode(parameters)].join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Retuns encoded HMAC-SHA1 from key and text
|
||||||
|
*/
|
||||||
|
OAuth.signature = function(text, key){
|
||||||
|
crypto = require("crypto");
|
||||||
|
return OAuth.encode(crypto.createHmac('sha1', key).update(text).digest('base64'));
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_token_secret){
|
||||||
|
oauth_parameters = oauth_parameters || {};
|
||||||
|
|
||||||
|
// Set default values
|
||||||
|
if (!oauth_parameters.oauth_nonce) {
|
||||||
|
oauth_parameters.oauth_nonce = OAuth.nonce();
|
||||||
|
}
|
||||||
|
if (!oauth_parameters.oauth_timestamp) {
|
||||||
|
oauth_parameters.oauth_timestamp = Math.floor(new Date().getTime()/1000);
|
||||||
|
}
|
||||||
|
if (!oauth_parameters.oauth_signature_method) {
|
||||||
|
oauth_parameters.oauth_signature_method = OAuth.signatureMethod;
|
||||||
|
}
|
||||||
|
if (!oauth_parameters.oauth_version) {
|
||||||
|
oauth_parameters.oauth_version = OAuth.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!auth_token_secret){
|
||||||
|
auth_token_secret="";
|
||||||
|
}
|
||||||
|
// Force GET method if unset
|
||||||
|
if (!request.method) {
|
||||||
|
request.method = "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all the parameters in one signatureParameters object
|
||||||
|
var signatureParams = {};
|
||||||
|
var parametersToMerge = [request.params, request.body, oauth_parameters];
|
||||||
|
for(var i in parametersToMerge) {
|
||||||
|
var parameters = parametersToMerge[i];
|
||||||
|
for(var k in parameters) {
|
||||||
|
signatureParams[k] = parameters[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a string based on the parameters
|
||||||
|
var parameterString = OAuth.buildParameterString(signatureParams);
|
||||||
|
|
||||||
|
// Build the signature string
|
||||||
|
var url = "https://"+request.host+""+request.path;
|
||||||
|
|
||||||
|
var signatureString = OAuth.buildSignatureString(request.method, url, parameterString);
|
||||||
|
// Hash the signature string
|
||||||
|
var signatureKey = [OAuth.encode(consumer_secret), OAuth.encode(auth_token_secret)].join("&");
|
||||||
|
|
||||||
|
var signature = OAuth.signature(signatureString, signatureKey);
|
||||||
|
|
||||||
|
// Set the signature in the params
|
||||||
|
oauth_parameters.oauth_signature = signature;
|
||||||
|
if(!request.headers){
|
||||||
|
request.headers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the authorization header
|
||||||
|
var signature = Object.keys(oauth_parameters).sort().map(function(key){
|
||||||
|
var value = oauth_parameters[key];
|
||||||
|
return key+'="'+value+'"';
|
||||||
|
}).join(", ")
|
||||||
|
|
||||||
|
request.headers.Authorization = 'OAuth ' + signature;
|
||||||
|
|
||||||
|
// Set the content type header
|
||||||
|
request.headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||||
|
return request;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = OAuth;
|
||||||
57
src/oauth/facebook.js
Normal file
57
src/oauth/facebook.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Helper functions for accessing the Facebook Graph API.
|
||||||
|
var https = require('https');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateAuthData(authData) {
|
||||||
|
return graphRequest('me?fields=id&access_token=' + authData.access_token)
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.id == authData.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Facebook auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
function validateAppId(appIds, access_token) {
|
||||||
|
if (!appIds.length) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Facebook auth is not configured.');
|
||||||
|
}
|
||||||
|
return graphRequest('app?access_token=' + access_token)
|
||||||
|
.then((data) => {
|
||||||
|
if (data && appIds.indexOf(data.id) != -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Facebook auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// A promisey wrapper for FB graph requests.
|
||||||
|
function graphRequest(path) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
https.get('https://graph.facebook.com/v2.5/' + path, function(res) {
|
||||||
|
var data = '';
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', function() {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
}).on('error', function(e) {
|
||||||
|
reject('Failed to validate this access token with Facebook.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateAppId: validateAppId,
|
||||||
|
validateAuthData: validateAuthData
|
||||||
|
};
|
||||||
51
src/oauth/github.js
Normal file
51
src/oauth/github.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Helper functions for accessing the github API.
|
||||||
|
var https = require('https');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateAuthData(authData) {
|
||||||
|
return request('user', authData.access_token)
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.id == authData.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Github auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
function validateAppId() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A promisey wrapper for api requests
|
||||||
|
function request(path, access_token) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
https.get({
|
||||||
|
host: 'api.github.com',
|
||||||
|
path: '/' + path,
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'bearer '+access_token,
|
||||||
|
'User-Agent': 'parse-server'
|
||||||
|
}
|
||||||
|
}, function(res) {
|
||||||
|
var data = '';
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', function() {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
}).on('error', function(e) {
|
||||||
|
reject('Failed to validate this access token with Github.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateAppId: validateAppId,
|
||||||
|
validateAuthData: validateAuthData
|
||||||
|
};
|
||||||
44
src/oauth/google.js
Normal file
44
src/oauth/google.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Helper functions for accessing the google API.
|
||||||
|
var https = require('https');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateAuthData(authData) {
|
||||||
|
return request("tokeninfo?access_token="+authData.access_token)
|
||||||
|
.then((response) => {
|
||||||
|
if (response && response.user_id == authData.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Google auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
function validateAppId() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A promisey wrapper for api requests
|
||||||
|
function request(path) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
https.get("https://www.googleapis.com/oauth2/v1/" + path, function(res) {
|
||||||
|
var data = '';
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', function() {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
}).on('error', function(e) {
|
||||||
|
reject('Failed to validate this access token with Google.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateAppId: validateAppId,
|
||||||
|
validateAuthData: validateAuthData
|
||||||
|
};
|
||||||
17
src/oauth/index.js
Normal file
17
src/oauth/index.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
var facebook = require('./facebook');
|
||||||
|
var instagram = require("./instagram");
|
||||||
|
var linkedin = require("./linkedin");
|
||||||
|
var meetup = require("./meetup");
|
||||||
|
var google = require("./google");
|
||||||
|
var github = require("./github");
|
||||||
|
var twitter = require("./twitter");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
facebook: facebook,
|
||||||
|
github: github,
|
||||||
|
google: google,
|
||||||
|
instagram: instagram,
|
||||||
|
linkedin: linkedin,
|
||||||
|
meetup: meetup,
|
||||||
|
twitter: twitter
|
||||||
|
}
|
||||||
44
src/oauth/instagram.js
Normal file
44
src/oauth/instagram.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Helper functions for accessing the instagram API.
|
||||||
|
var https = require('https');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateAuthData(authData) {
|
||||||
|
return request("users/self/?access_token="+authData.access_token)
|
||||||
|
.then((response) => {
|
||||||
|
if (response && response.data && response.data.id == authData.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Instagram auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
function validateAppId() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A promisey wrapper for api requests
|
||||||
|
function request(path) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
https.get("https://api.instagram.com/v1/" + path, function(res) {
|
||||||
|
var data = '';
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', function() {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
}).on('error', function(e) {
|
||||||
|
reject('Failed to validate this access token with Instagram.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateAppId: validateAppId,
|
||||||
|
validateAuthData: validateAuthData
|
||||||
|
};
|
||||||
51
src/oauth/linkedin.js
Normal file
51
src/oauth/linkedin.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Helper functions for accessing the linkedin API.
|
||||||
|
var https = require('https');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateAuthData(authData) {
|
||||||
|
return request('people/~:(id)', authData.access_token)
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.id == authData.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Meetup auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
function validateAppId() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A promisey wrapper for api requests
|
||||||
|
function request(path, access_token) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
https.get({
|
||||||
|
host: 'api.linkedin.com',
|
||||||
|
path: '/v1/' + path,
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
'x-li-format': 'json'
|
||||||
|
}
|
||||||
|
}, function(res) {
|
||||||
|
var data = '';
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', function() {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
}).on('error', function(e) {
|
||||||
|
reject('Failed to validate this access token with Linkedin.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateAppId: validateAppId,
|
||||||
|
validateAuthData: validateAuthData
|
||||||
|
};
|
||||||
50
src/oauth/meetup.js
Normal file
50
src/oauth/meetup.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Helper functions for accessing the meetup API.
|
||||||
|
var https = require('https');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateAuthData(authData) {
|
||||||
|
return request('member/self', authData.access_token)
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.id == authData.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Meetup auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
function validateAppId() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A promisey wrapper for api requests
|
||||||
|
function request(path, access_token) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
https.get({
|
||||||
|
host: 'api.meetup.com',
|
||||||
|
path: '/2/' + path,
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'bearer '+access_token
|
||||||
|
}
|
||||||
|
}, function(res) {
|
||||||
|
var data = '';
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', function() {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
}).on('error', function(e) {
|
||||||
|
reject('Failed to validate this access token with Meetup.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateAppId: validateAppId,
|
||||||
|
validateAuthData: validateAuthData
|
||||||
|
};
|
||||||
30
src/oauth/twitter.js
Normal file
30
src/oauth/twitter.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Helper functions for accessing the meetup API.
|
||||||
|
var OAuth = require('./OAuth1Client');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateAuthData(authData, options) {
|
||||||
|
var client = new OAuth(options);
|
||||||
|
client.host = "api.twitter.com";
|
||||||
|
client.auth_token = authData.auth_token;
|
||||||
|
client.auth_token_secret = authData.auth_token_secret;
|
||||||
|
|
||||||
|
return client.get("/1.1/account/verify_credentials.json").then((data) => {
|
||||||
|
if (data && data.id == authData.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Twitter auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
function validateAppId() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateAppId: validateAppId,
|
||||||
|
validateAuthData: validateAuthData
|
||||||
|
};
|
||||||
149
src/schemas.js
149
src/schemas.js
@@ -7,36 +7,27 @@ var express = require('express'),
|
|||||||
|
|
||||||
var router = new PromiseRouter();
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
function mongoFieldTypeToSchemaAPIType(type) {
|
function masterKeyRequiredResponse() {
|
||||||
if (type[0] === '*') {
|
return Promise.resolve({
|
||||||
return {
|
status: 401,
|
||||||
type: 'Pointer',
|
response: {error: 'master key not specified'},
|
||||||
targetClass: type.slice(1),
|
})
|
||||||
};
|
}
|
||||||
}
|
|
||||||
if (type.startsWith('relation<')) {
|
function classNameMismatchResponse(bodyClass, pathClass) {
|
||||||
return {
|
return Promise.resolve({
|
||||||
type: 'Relation',
|
status: 400,
|
||||||
targetClass: type.slice('relation<'.length, type.length - 1),
|
response: {
|
||||||
};
|
code: Parse.Error.INVALID_CLASS_NAME,
|
||||||
}
|
error: 'class name mismatch between ' + bodyClass + ' and ' + pathClass,
|
||||||
switch (type) {
|
}
|
||||||
case 'number': return {type: 'Number'};
|
});
|
||||||
case 'string': return {type: 'String'};
|
|
||||||
case 'boolean': return {type: 'Boolean'};
|
|
||||||
case 'date': return {type: 'Date'};
|
|
||||||
case 'map':
|
|
||||||
case 'object': return {type: 'Object'};
|
|
||||||
case 'array': return {type: 'Array'};
|
|
||||||
case 'geopoint': return {type: 'GeoPoint'};
|
|
||||||
case 'file': return {type: 'File'};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mongoSchemaAPIResponseFields(schema) {
|
function mongoSchemaAPIResponseFields(schema) {
|
||||||
var fieldNames = Object.keys(schema).filter(key => key !== '_id' && key !== '_metadata');
|
var fieldNames = Object.keys(schema).filter(key => key !== '_id' && key !== '_metadata');
|
||||||
var response = fieldNames.reduce((obj, fieldName) => {
|
var response = fieldNames.reduce((obj, fieldName) => {
|
||||||
obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName])
|
obj[fieldName] = Schema.mongoFieldTypeToSchemaAPIType(schema[fieldName])
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
response.ACL = {type: 'ACL'};
|
response.ACL = {type: 'ACL'};
|
||||||
@@ -55,10 +46,7 @@ function mongoSchemaToSchemaAPIResponse(schema) {
|
|||||||
|
|
||||||
function getAllSchemas(req) {
|
function getAllSchemas(req) {
|
||||||
if (!req.auth.isMaster) {
|
if (!req.auth.isMaster) {
|
||||||
return Promise.resolve({
|
return masterKeyRequiredResponse();
|
||||||
status: 401,
|
|
||||||
response: {error: 'master key not specified'},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return req.config.database.collection('_SCHEMA')
|
return req.config.database.collection('_SCHEMA')
|
||||||
.then(coll => coll.find({}).toArray())
|
.then(coll => coll.find({}).toArray())
|
||||||
@@ -69,10 +57,7 @@ function getAllSchemas(req) {
|
|||||||
|
|
||||||
function getOneSchema(req) {
|
function getOneSchema(req) {
|
||||||
if (!req.auth.isMaster) {
|
if (!req.auth.isMaster) {
|
||||||
return Promise.resolve({
|
return masterKeyRequiredResponse();
|
||||||
status: 401,
|
|
||||||
response: {error: 'unauthorized'},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return req.config.database.collection('_SCHEMA')
|
return req.config.database.collection('_SCHEMA')
|
||||||
.then(coll => coll.findOne({'_id': req.params.className}))
|
.then(coll => coll.findOne({'_id': req.params.className}))
|
||||||
@@ -88,20 +73,11 @@ function getOneSchema(req) {
|
|||||||
|
|
||||||
function createSchema(req) {
|
function createSchema(req) {
|
||||||
if (!req.auth.isMaster) {
|
if (!req.auth.isMaster) {
|
||||||
return Promise.resolve({
|
return masterKeyRequiredResponse();
|
||||||
status: 401,
|
|
||||||
response: {error: 'master key not specified'},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (req.params.className && req.body.className) {
|
if (req.params.className && req.body.className) {
|
||||||
if (req.params.className != req.body.className) {
|
if (req.params.className != req.body.className) {
|
||||||
return Promise.resolve({
|
return classNameMismatchResponse(req.body.className, req.params.className);
|
||||||
status: 400,
|
|
||||||
response: {
|
|
||||||
code: Parse.Error.INVALID_CLASS_NAME,
|
|
||||||
error: 'class name mismatch between ' + req.body.className + ' and ' + req.params.className,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var className = req.params.className || req.body.className;
|
var className = req.params.className || req.body.className;
|
||||||
@@ -123,9 +99,94 @@ function createSchema(req) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modifySchema(req) {
|
||||||
|
if (!req.auth.isMaster) {
|
||||||
|
return masterKeyRequiredResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.body.className && req.body.className != req.params.className) {
|
||||||
|
return classNameMismatchResponse(req.body.className, req.params.className);
|
||||||
|
}
|
||||||
|
|
||||||
|
var submittedFields = req.body.fields || {};
|
||||||
|
var className = req.params.className;
|
||||||
|
|
||||||
|
return req.config.database.loadSchema()
|
||||||
|
.then(schema => {
|
||||||
|
if (!schema.data[className]) {
|
||||||
|
return Promise.resolve({
|
||||||
|
status: 400,
|
||||||
|
response: {
|
||||||
|
code: Parse.Error.INVALID_CLASS_NAME,
|
||||||
|
error: 'class ' + req.params.className + ' does not exist',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var existingFields = schema.data[className];
|
||||||
|
|
||||||
|
for (var submittedFieldName in submittedFields) {
|
||||||
|
if (existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op !== 'Delete') {
|
||||||
|
return Promise.resolve({
|
||||||
|
status: 400,
|
||||||
|
response: {
|
||||||
|
code: 255,
|
||||||
|
error: 'field ' + submittedFieldName + ' exists, cannot update',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op === 'Delete') {
|
||||||
|
return Promise.resolve({
|
||||||
|
status: 400,
|
||||||
|
response: {
|
||||||
|
code: 255,
|
||||||
|
error: 'field ' + submittedFieldName + ' does not exist, cannot delete',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields);
|
||||||
|
var mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className);
|
||||||
|
if (!mongoObject.result) {
|
||||||
|
return Promise.resolve({
|
||||||
|
status: 400,
|
||||||
|
response: mongoObject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally we have checked to make sure the request is valid and we can start deleting fields.
|
||||||
|
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
||||||
|
var deletionPromises = []
|
||||||
|
Object.keys(submittedFields).forEach(submittedFieldName => {
|
||||||
|
if (submittedFields[submittedFieldName].__op === 'Delete') {
|
||||||
|
var promise = req.config.database.connect()
|
||||||
|
.then(() => schema.deleteField(
|
||||||
|
submittedFieldName,
|
||||||
|
className,
|
||||||
|
req.config.database.db,
|
||||||
|
req.config.database.collectionPrefix
|
||||||
|
));
|
||||||
|
deletionPromises.push(promise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(deletionPromises)
|
||||||
|
.then(() => new Promise((resolve, reject) => {
|
||||||
|
schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)});
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
router.route('GET', '/schemas', getAllSchemas);
|
router.route('GET', '/schemas', getAllSchemas);
|
||||||
router.route('GET', '/schemas/:className', getOneSchema);
|
router.route('GET', '/schemas/:className', getOneSchema);
|
||||||
router.route('POST', '/schemas', createSchema);
|
router.route('POST', '/schemas', createSchema);
|
||||||
router.route('POST', '/schemas/:className', createSchema);
|
router.route('POST', '/schemas/:className', createSchema);
|
||||||
|
router.route('PUT', '/schemas/:className', modifySchema);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -55,21 +55,6 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
|||||||
case '_wperm':
|
case '_wperm':
|
||||||
return {key: key, value: restValue};
|
return {key: key, value: restValue};
|
||||||
break;
|
break;
|
||||||
case 'authData.anonymous.id':
|
|
||||||
if (options.query) {
|
|
||||||
return {key: '_auth_data_anonymous.id', value: restValue};
|
|
||||||
}
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
|
||||||
'can only query on ' + key);
|
|
||||||
break;
|
|
||||||
case 'authData.facebook.id':
|
|
||||||
if (options.query) {
|
|
||||||
// Special-case auth data.
|
|
||||||
return {key: '_auth_data_facebook.id', value: restValue};
|
|
||||||
}
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
|
||||||
'can only query on ' + key);
|
|
||||||
break;
|
|
||||||
case '$or':
|
case '$or':
|
||||||
if (!options.query) {
|
if (!options.query) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||||
@@ -97,6 +82,18 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
|||||||
});
|
});
|
||||||
return {key: '$and', value: mongoSubqueries};
|
return {key: '$and', value: mongoSubqueries};
|
||||||
default:
|
default:
|
||||||
|
// Other auth data
|
||||||
|
var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
|
||||||
|
if (authDataMatch) {
|
||||||
|
if (options.query) {
|
||||||
|
var provider = authDataMatch[1];
|
||||||
|
// Special-case auth data.
|
||||||
|
return {key: '_auth_data_'+provider+'.id', value: restValue};
|
||||||
|
}
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||||
|
'can only query on ' + key);
|
||||||
|
break;
|
||||||
|
};
|
||||||
if (options.validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
|
if (options.validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||||
'invalid key name: ' + key);
|
'invalid key name: ' + key);
|
||||||
@@ -646,15 +643,16 @@ function untransformObject(schema, className, mongoObject) {
|
|||||||
case '_expiresAt':
|
case '_expiresAt':
|
||||||
restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key])).iso;
|
restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key])).iso;
|
||||||
break;
|
break;
|
||||||
case '_auth_data_anonymous':
|
|
||||||
restObject['authData'] = restObject['authData'] || {};
|
|
||||||
restObject['authData']['anonymous'] = mongoObject[key];
|
|
||||||
break;
|
|
||||||
case '_auth_data_facebook':
|
|
||||||
restObject['authData'] = restObject['authData'] || {};
|
|
||||||
restObject['authData']['facebook'] = mongoObject[key];
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
|
// Check other auth data keys
|
||||||
|
var authDataMatch = key.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
|
||||||
|
if (authDataMatch) {
|
||||||
|
var provider = authDataMatch[1];
|
||||||
|
restObject['authData'] = restObject['authData'] || {};
|
||||||
|
restObject['authData'][provider] = mongoObject[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (key.indexOf('_p_') == 0) {
|
if (key.indexOf('_p_') == 0) {
|
||||||
var newKey = key.substring(3);
|
var newKey = key.substring(3);
|
||||||
var expected;
|
var expected;
|
||||||
|
|||||||
Reference in New Issue
Block a user