diff --git a/package.json b/package.json
index 9837376d53..560e8e99ef 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"commander": "^2.9.0",
"deepcopy": "^0.6.1",
"express": "^4.13.4",
+ "mailgun-js": "^0.7.7",
"mime": "^1.3.4",
"mongodb": "~2.1.0",
"multer": "^1.1.0",
diff --git a/public_html/invalid_link.html b/public_html/invalid_link.html
new file mode 100644
index 0000000000..66bdc788fb
--- /dev/null
+++ b/public_html/invalid_link.html
@@ -0,0 +1,43 @@
+
+
+
+
+ Invalid Link
+
+
+
+
Invalid Link
+
+
+
diff --git a/public_html/password_reset_success.html b/public_html/password_reset_success.html
new file mode 100644
index 0000000000..774cbb350c
--- /dev/null
+++ b/public_html/password_reset_success.html
@@ -0,0 +1,27 @@
+
+
+
+
+ Password Reset
+
+
+ Successfully updated your password!
+
+
diff --git a/public_html/verify_email_success.html b/public_html/verify_email_success.html
new file mode 100644
index 0000000000..774ea38a0d
--- /dev/null
+++ b/public_html/verify_email_success.html
@@ -0,0 +1,27 @@
+
+
+
+
+ Email Verification
+
+
+ Successfully verified your email!
+
+
diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js
index 80f30d6fed..f32867e0bc 100644
--- a/spec/AdapterLoader.spec.js
+++ b/spec/AdapterLoader.spec.js
@@ -2,15 +2,17 @@
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
-describe("AdaptableController", ()=>{
+describe("AdapterLoader", ()=>{
it("should instantiate an adapter from string in object", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var adapter = loadAdapter({
adapter: adapterPath,
- key: "value",
- foo: "bar"
+ options: {
+ key: "value",
+ foo: "bar"
+ }
});
expect(adapter instanceof Object).toBe(true);
@@ -24,7 +26,6 @@ describe("AdaptableController", ()=>{
var adapter = loadAdapter(adapterPath);
expect(adapter instanceof Object).toBe(true);
- expect(adapter.options).toBe(adapterPath);
done();
});
@@ -65,4 +66,22 @@ describe("AdaptableController", ()=>{
expect(adapter).toBe(originalAdapter);
done();
});
+
+ it("should fail loading an improperly configured adapter", (done) => {
+ var Adapter = function(options) {
+ if (!options.foo) {
+ throw "foo is required for that adapter";
+ }
+ }
+ var adapterOptions = {
+ param: "key",
+ doSomething: function() {}
+ };
+
+ expect(() => {
+ var adapter = loadAdapter(adapterOptions, Adapter);
+ expect(adapter).toEqual(adapterOptions);
+ }).not.toThrow("foo is required for that adapter");
+ done();
+ });
});
diff --git a/spec/MockAdapter.js b/spec/MockAdapter.js
index 60d8ef8686..c3f557849d 100644
--- a/spec/MockAdapter.js
+++ b/spec/MockAdapter.js
@@ -1,3 +1,5 @@
module.exports = function(options) {
- this.options = options;
-}
+ return {
+ options: options
+ };
+};
diff --git a/spec/MockEmailAdapter.js b/spec/MockEmailAdapter.js
new file mode 100644
index 0000000000..b143e37e6e
--- /dev/null
+++ b/spec/MockEmailAdapter.js
@@ -0,0 +1,5 @@
+module.exports = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => Promise.resolve()
+}
diff --git a/spec/MockEmailAdapterWithOptions.js b/spec/MockEmailAdapterWithOptions.js
new file mode 100644
index 0000000000..8a3095e21f
--- /dev/null
+++ b/spec/MockEmailAdapterWithOptions.js
@@ -0,0 +1,10 @@
+module.exports = options => {
+ if (!options) {
+ throw "Options were not provided"
+ }
+ return {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => Promise.resolve()
+ }
+}
diff --git a/spec/OneSignalPushAdapter.spec.js b/spec/OneSignalPushAdapter.spec.js
index 2c165c4588..a9b853d98d 100644
--- a/spec/OneSignalPushAdapter.spec.js
+++ b/spec/OneSignalPushAdapter.spec.js
@@ -1,13 +1,15 @@
var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter');
var classifyInstallations = require('../src/Adapters/Push/PushAdapterUtils').classifyInstallations;
+
+// Make mock config
+var pushConfig = {
+ oneSignalAppId:"APP ID",
+ oneSignalApiKey:"API KEY"
+};
+
describe('OneSignalPushAdapter', () => {
it('can be initialized', (done) => {
- // Make mock config
- var pushConfig = {
- oneSignalAppId:"APP ID",
- oneSignalApiKey:"API KEY"
- };
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
@@ -17,9 +19,17 @@ describe('OneSignalPushAdapter', () => {
expect(senderMap.android instanceof Function).toBe(true);
done();
});
+
+ it('cannot be initialized if options are missing', (done) => {
+
+ expect(() => {
+ new OneSignalPushAdapter();
+ }).toThrow("Trying to initialize OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey");
+ done();
+ });
it('can get valid push types', (done) => {
- var oneSignalPushAdapter = new OneSignalPushAdapter();
+ var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
done();
@@ -56,7 +66,7 @@ describe('OneSignalPushAdapter', () => {
it('can send push notifications', (done) => {
- var oneSignalPushAdapter = new OneSignalPushAdapter();
+ var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
// Mock android ios senders
var androidSender = jasmine.createSpy('send')
@@ -108,7 +118,7 @@ describe('OneSignalPushAdapter', () => {
});
it("can send iOS notifications", (done) => {
- var oneSignalPushAdapter = new OneSignalPushAdapter();
+ var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
@@ -135,7 +145,7 @@ describe('OneSignalPushAdapter', () => {
});
it("can send Android notifications", (done) => {
- var oneSignalPushAdapter = new OneSignalPushAdapter();
+ var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
@@ -157,10 +167,7 @@ describe('OneSignalPushAdapter', () => {
});
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');
diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js
index 8c29ee4835..8b739a785a 100644
--- a/spec/ParseGlobalConfig.spec.js
+++ b/spec/ParseGlobalConfig.spec.js
@@ -2,13 +2,12 @@
var request = require('request');
var Parse = require('parse/node').Parse;
-var DatabaseAdapter = require('../src/DatabaseAdapter');
-
-let database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
+let Config = require('../src/Config');
describe('a GlobalConfig', () => {
beforeEach(function(done) {
- database.rawCollection('_GlobalConfig')
+ let config = new Config('test');
+ config.database.rawCollection('_GlobalConfig')
.then(coll => coll.updateOne({ '_id': 1}, { $set: { params: { companies: ['US', 'DK'] } } }, { upsert: true }))
.then(done());
});
@@ -61,7 +60,8 @@ describe('a GlobalConfig', () => {
});
it('failed getting config when it is missing', (done) => {
- database.rawCollection('_GlobalConfig')
+ let config = new Config('test');
+ config.database.rawCollection('_GlobalConfig')
.then(coll => coll.deleteOne({ '_id': 1}, {}, {}))
.then(_ => {
request.get({
diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js
index a36b3cdcba..58c9e8f319 100644
--- a/spec/ParseUser.spec.js
+++ b/spec/ParseUser.spec.js
@@ -54,6 +54,11 @@ describe('Parse.User testing', () => {
success: function(user) {
Parse.User.logIn("non_existent_user", "asdf3",
expectError(Parse.Error.OBJECT_NOT_FOUND, done));
+ },
+ error: function(err) {
+ console.error(err);
+ fail("Shit should not fail");
+ done();
}
});
});
@@ -1704,7 +1709,7 @@ describe('Parse.User testing', () => {
done();
});
});
-
+
it('test parse user become', (done) => {
var sessionToken = null;
Parse.Promise.as().then(function() {
diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js
new file mode 100644
index 0000000000..008d544ae4
--- /dev/null
+++ b/spec/PublicAPI.spec.js
@@ -0,0 +1,86 @@
+
+var request = require('request');
+
+describe("public API", () => {
+ beforeEach(done => {
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ publicServerURL: 'http://localhost:8378/1'
+ });
+ done();
+ })
+ it("should get invalid_link.html", (done) => {
+ request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse, body) => {
+ expect(httpResponse.statusCode).toBe(200);
+ done();
+ });
+ });
+
+ it("should get choose_password", (done) => {
+ request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
+ expect(httpResponse.statusCode).toBe(200);
+ done();
+ });
+ });
+
+ it("should get verify_email_success.html", (done) => {
+ request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse, body) => {
+ expect(httpResponse.statusCode).toBe(200);
+ done();
+ });
+ });
+
+ it("should get password_reset_success.html", (done) => {
+ request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse, body) => {
+ expect(httpResponse.statusCode).toBe(200);
+ done();
+ });
+ });
+});
+
+describe("public API without publicServerURL", () => {
+ beforeEach(done => {
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ });
+ done();
+ })
+ it("should get 404 on verify_email", (done) => {
+ request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse, body) => {
+ expect(httpResponse.statusCode).toBe(404);
+ done();
+ });
+ });
+
+ it("should get 404 choose_password", (done) => {
+ request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
+ expect(httpResponse.statusCode).toBe(404);
+ done();
+ });
+ });
+
+ it("should get 404 on request_password_reset", (done) => {
+ request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse, body) => {
+ expect(httpResponse.statusCode).toBe(404);
+ done();
+ });
+ });
+});
diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js
new file mode 100644
index 0000000000..6ac874cd4d
--- /dev/null
+++ b/spec/ValidationAndPasswordsReset.spec.js
@@ -0,0 +1,618 @@
+"use strict";
+
+var request = require('request');
+var Config = require("../src/Config");
+describe("Custom Pages Configuration", () => {
+ it("should set the custom pages", (done) => {
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ customPages: {
+ invalidLink: "myInvalidLink",
+ verifyEmailSuccess: "myVerifyEmailSuccess",
+ choosePassword: "myChoosePassword",
+ passwordResetSuccess: "myPasswordResetSuccess"
+ },
+ publicServerURL: "https://my.public.server.com/1"
+ });
+
+ var config = new Config("test");
+
+ expect(config.invalidLinkURL).toEqual("myInvalidLink");
+ expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
+ expect(config.choosePasswordURL).toEqual("myChoosePassword");
+ expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
+ expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
+ expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
+ done();
+ });
+});
+
+describe("Email Verification", () => {
+ it('sends verification email if email verification is enabled', done => {
+ var emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => Promise.resolve()
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ publicServerURL: "http://localhost:8378/1"
+ });
+ spyOn(emailAdapter, 'sendVerificationEmail');
+ var user = new Parse.User();
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.setEmail('cool_guy@parse.com');
+ user.signUp(null, {
+ success: function(user) {
+ expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
+ user.fetch()
+ .then(() => {
+ expect(user.get('emailVerified')).toEqual(false);
+ done();
+ });
+ },
+ error: function(userAgain, error) {
+ fail('Failed to save user');
+ done();
+ }
+ });
+ });
+
+ it('does not send verification email when verification is enabled and email is not set', done => {
+ var emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => Promise.resolve()
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ publicServerURL: "http://localhost:8378/1"
+ });
+ spyOn(emailAdapter, 'sendVerificationEmail');
+ var user = new Parse.User();
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.signUp(null, {
+ success: function(user) {
+ expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
+ user.fetch()
+ .then(() => {
+ expect(user.get('emailVerified')).toEqual(undefined);
+ done();
+ });
+ },
+ error: function(userAgain, error) {
+ fail('Failed to save user');
+ done();
+ }
+ });
+ });
+
+ it('does send a validation email when updating the email', done => {
+ var emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => Promise.resolve()
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ publicServerURL: "http://localhost:8378/1"
+ });
+ spyOn(emailAdapter, 'sendVerificationEmail');
+ var user = new Parse.User();
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.signUp(null, {
+ success: function(user) {
+ expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
+ user.fetch()
+ .then((user) => {
+ user.set("email", "cool_guy@parse.com");
+ return user.save();
+ }).then((user) => {
+ return user.fetch();
+ }).then(() => {
+ expect(user.get('emailVerified')).toEqual(false);
+ // Wait as on update emai, we need to fetch the username
+ setTimeout(function(){
+ expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
+ done();
+ }, 200);
+ });
+ },
+ error: function(userAgain, error) {
+ fail('Failed to save user');
+ done();
+ }
+ });
+ });
+
+ it('does send with a simple adapter', done => {
+ var calls = 0;
+ var emailAdapter = {
+ sendMail: function(options){
+ expect(options.to).toBe('cool_guy@parse.com');
+ if (calls == 0) {
+ expect(options.subject).toEqual('Please verify your e-mail for My Cool App');
+ expect(options.text.match(/verify_email/)).not.toBe(null);
+ } else if (calls == 1) {
+ expect(options.subject).toEqual('Password Reset for My Cool App');
+ expect(options.text.match(/request_password_reset/)).not.toBe(null);
+ }
+ calls++;
+ return Promise.resolve();
+ }
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'My Cool App',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ publicServerURL: "http://localhost:8378/1"
+ });
+ var user = new Parse.User();
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.set("email", "cool_guy@parse.com");
+ user.signUp(null, {
+ success: function(user) {
+ expect(calls).toBe(1);
+ user.fetch()
+ .then((user) => {
+ return user.save();
+ }).then((user) => {
+ return Parse.User.requestPasswordReset("cool_guy@parse.com");
+ }).then(() => {
+ expect(calls).toBe(2);
+ done();
+ });
+ },
+ error: function(userAgain, error) {
+ fail('Failed to save user');
+ done();
+ }
+ });
+ });
+
+ it('does not send verification email if email verification is disabled', done => {
+ var emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => Promise.resolve()
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: false,
+ emailAdapter: emailAdapter,
+ });
+ spyOn(emailAdapter, 'sendVerificationEmail');
+ var user = new Parse.User();
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.signUp(null, {
+ success: function(user) {
+ user.fetch()
+ .then(() => {
+ expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0);
+ expect(user.get('emailVerified')).toEqual(undefined);
+ done();
+ });
+ },
+ error: function(userAgain, error) {
+ fail('Failed to save user');
+ done();
+ }
+ });
+ });
+
+ it('receives the app name and user in the adapter', done => {
+ var emailAdapter = {
+ sendVerificationEmail: options => {
+ expect(options.appName).toEqual('emailing app');
+ expect(options.user.get('email')).toEqual('user@parse.com');
+ done();
+ },
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {}
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'emailing app',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ publicServerURL: "http://localhost:8378/1"
+ });
+ var user = new Parse.User();
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.set('email', 'user@parse.com');
+ user.signUp(null, {
+ success: () => {},
+ error: function(userAgain, error) {
+ fail('Failed to save user');
+ done();
+ }
+ });
+ })
+
+ it('when you click the link in the email it sets emailVerified to true and redirects you', done => {
+ var user = new Parse.User();
+ var emailAdapter = {
+ sendVerificationEmail: options => {
+ request.get(options.link, {
+ followRedirect: false,
+ }, (error, response, body) => {
+ expect(response.statusCode).toEqual(302);
+ expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=zxcv');
+ user.fetch()
+ .then(() => {
+ expect(user.get('emailVerified')).toEqual(true);
+ done();
+ }, (err) => {
+ console.error(err);
+ fail("this should not fail");
+ done();
+ });
+ });
+ },
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {}
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'emailing app',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ publicServerURL: "http://localhost:8378/1"
+ });
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.set('email', 'user@parse.com');
+ user.signUp();
+ });
+
+ it('redirects you to invalid link if you try to verify email incorrecly', done => {
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'emailing app',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {}
+ },
+ publicServerURL: "http://localhost:8378/1"
+ });
+ request.get('http://localhost:8378/1/apps/test/verify_email', {
+ followRedirect: false,
+ }, (error, response, body) => {
+ expect(response.statusCode).toEqual(302);
+ expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
+ done()
+ });
+ });
+
+ it('redirects you to invalid link if you try to validate a nonexistant users email', done => {
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'emailing app',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {}
+ },
+ publicServerURL: "http://localhost:8378/1"
+ });
+ request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', {
+ followRedirect: false,
+ }, (error, response, body) => {
+ expect(response.statusCode).toEqual(302);
+ expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
+ done();
+ });
+ });
+
+ it('does not update email verified if you use an invalid token', done => {
+ var user = new Parse.User();
+ var emailAdapter = {
+ sendVerificationEmail: options => {
+ request.get('http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', {
+ followRedirect: false,
+ }, (error, response, body) => {
+ expect(response.statusCode).toEqual(302);
+ expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
+ user.fetch()
+ .then(() => {
+ expect(user.get('emailVerified')).toEqual(false);
+ done();
+ });
+ });
+ },
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {}
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'emailing app',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ publicServerURL: "http://localhost:8378/1"
+ });
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.set('email', 'user@parse.com');
+ user.signUp(null, {
+ success: () => {},
+ error: function(userAgain, error) {
+ fail('Failed to save user');
+ done();
+ }
+ });
+ });
+});
+
+describe("Password Reset", () => {
+
+ it('should send a password reset link', done => {
+ var user = new Parse.User();
+ var emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: options => {
+ request.get(options.link, {
+ followRedirect: false,
+ }, (error, response, body) => {
+ if (error) {
+ console.error(error);
+ fail("Failed to get the reset link");
+ return;
+ }
+ expect(response.statusCode).toEqual(302);
+ var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv/;
+ expect(response.body.match(re)).not.toBe(null);
+ done();
+ });
+ },
+ sendMail: () => {}
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'emailing app',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ publicServerURL: "http://localhost:8378/1"
+ });
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.set('email', 'user@parse.com');
+ user.signUp().then(() => {
+ Parse.User.requestPasswordReset('user@parse.com', {
+ error: (err) => {
+ console.error(err);
+ fail("Should not fail");
+ done();
+ }
+ });
+ });
+ });
+
+ it('redirects you to invalid link if you try to request password for a nonexistant users email', done => {
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'emailing app',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {}
+ },
+ publicServerURL: "http://localhost:8378/1"
+ });
+ request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', {
+ followRedirect: false,
+ }, (error, response, body) => {
+ expect(response.statusCode).toEqual(302);
+ expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
+ done();
+ });
+ });
+
+ it('should programatically reset password', done => {
+ var user = new Parse.User();
+ var emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: options => {
+ request.get(options.link, {
+ followRedirect: false,
+ }, (error, response, body) => {
+ if (error) {
+ console.error(error);
+ fail("Failed to get the reset link");
+ return;
+ }
+ expect(response.statusCode).toEqual(302);
+ var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/;
+ var match = response.body.match(re);
+ if (!match) {
+ fail("should have a token");
+ done();
+ return;
+ }
+ var token = match[1];
+
+ request.post({
+ url: "http://localhost:8378/1/apps/test/request_password_reset" ,
+ body: `new_password=hello&token=${token}&username=zxcv`,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ followRedirect: false,
+ }, (error, response, body) => {
+ if (error) {
+ console.error(error);
+ fail("Failed to POST request password reset");
+ return;
+ }
+ expect(response.statusCode).toEqual(302);
+ expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html');
+
+ Parse.User.logIn("zxcv", "hello").then(function(user){
+ done();
+ }, (err) => {
+ console.error(err);
+ fail("should login with new password");
+ done();
+ });
+
+ });
+ });
+ },
+ sendMail: () => {}
+ }
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'emailing app',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ publicServerURL: "http://localhost:8378/1"
+ });
+ user.setPassword("asdf");
+ user.setUsername("zxcv");
+ user.set('email', 'user@parse.com');
+ user.signUp().then(() => {
+ Parse.User.requestPasswordReset('user@parse.com', {
+ error: (err) => {
+ console.error(err);
+ fail("Should not fail");
+ done();
+ }
+ });
+ });
+ });
+
+})
+
diff --git a/spec/helper.js b/spec/helper.js
index 92231393ce..e2daa6ed25 100644
--- a/spec/helper.js
+++ b/spec/helper.js
@@ -250,3 +250,4 @@ global.arrayContains = arrayContains;
global.jequal = jequal;
global.range = range;
global.setServerConfiguration = setServerConfiguration;
+global.defaultConfiguration = defaultConfiguration;
diff --git a/spec/index.spec.js b/spec/index.spec.js
index 8b55808997..e3e2cb0bd3 100644
--- a/spec/index.spec.js
+++ b/spec/index.spec.js
@@ -1,4 +1,5 @@
var request = require('request');
+var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
describe('server', () => {
it('requires a master key and app id', done => {
@@ -37,4 +38,119 @@ describe('server', () => {
done();
});
});
+
+ it('can load email adapter via object', done => {
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: MockEmailAdapterWithOptions({
+ apiKey: 'k',
+ domain: 'd',
+ }),
+ publicServerURL: 'http://localhost:8378/1'
+ });
+ done();
+ });
+
+ it('can load email adapter via class', done => {
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: {
+ class: MockEmailAdapterWithOptions,
+ options: {
+ apiKey: 'k',
+ domain: 'd',
+ }
+ },
+ publicServerURL: 'http://localhost:8378/1'
+ });
+ done();
+ });
+
+ it('can load email adapter via module name', done => {
+ setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: {
+ module: './Email/SimpleMailgunAdapter',
+ options: {
+ apiKey: 'k',
+ domain: 'd',
+ }
+ },
+ publicServerURL: 'http://localhost:8378/1'
+ });
+ done();
+ });
+
+ it('can load email adapter via only module name', done => {
+ expect(() => setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: './Email/SimpleMailgunAdapter',
+ publicServerURL: 'http://localhost:8378/1'
+ })).toThrow('SimpleMailgunAdapter requires an API Key and domain.');
+ done();
+ });
+
+ it('throws if you initialize email adapter incorrecly', done => {
+ expect(() => setServerConfiguration({
+ serverURL: 'http://localhost:8378/1',
+ appId: 'test',
+ appName: 'unused',
+ javascriptKey: 'test',
+ dotNetKey: 'windows',
+ clientKey: 'client',
+ restAPIKey: 'rest',
+ masterKey: 'test',
+ collectionPrefix: 'test_',
+ fileKey: 'test',
+ verifyUserEmails: true,
+ emailAdapter: {
+ module: './Email/SimpleMailgunAdapter',
+ options: {
+ domain: 'd',
+ }
+ },
+ publicServerURL: 'http://localhost:8378/1'
+ })).toThrow('SimpleMailgunAdapter requires an API Key and domain.');
+ done();
+ });
});
diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js
index cfe51ffd7e..5b46f22d2d 100644
--- a/src/Adapters/AdapterLoader.js
+++ b/src/Adapters/AdapterLoader.js
@@ -1,35 +1,43 @@
+export function loadAdapter(adapter, defaultAdapter, options) {
-export function loadAdapter(options, defaultAdapter) {
- let adapter;
-
- // We have options and options have adapter key
- if (options) {
- // Pass an adapter as a module name, a function or an instance
- if (typeof options == "string" || typeof options == "function" || options.constructor != Object) {
- adapter = options;
+ if (!adapter)
+ {
+ if (!defaultAdapter) {
+ return options;
}
- if (options.adapter) {
- adapter = options.adapter;
+ // Load from the default adapter when no adapter is set
+ return loadAdapter(defaultAdapter, undefined, options);
+ } else if (typeof adapter === "function") {
+ try {
+ return adapter(options);
+ } catch(e) {
+ var Adapter = adapter;
+ return new Adapter(options);
}
- }
-
- if (!adapter) {
- adapter = defaultAdapter;
- }
-
- // This is a string, require the module
- if (typeof adapter === "string") {
+ } else if (typeof adapter === "string") {
adapter = require(adapter);
// If it's define as a module, get the default
if (adapter.default) {
adapter = adapter.default;
}
+
+ return loadAdapter(adapter, undefined, options);
+ } else if (adapter.module) {
+ return loadAdapter(adapter.module, undefined, adapter.options);
+ } else if (adapter.class) {
+ return loadAdapter(adapter.class, undefined, adapter.options);
+ } else if (adapter.adapter) {
+ return loadAdapter(adapter.adapter, undefined, adapter.options);
+ } else {
+ // Try to load the defaultAdapter with the options
+ // The default adapter should throw if the options are
+ // incompatible
+ try {
+ return loadAdapter(defaultAdapter, undefined, adapter);
+ } catch (e) {};
}
- // From there it's either a function or an object
- // if it's an function, instanciate and pass the options
- if (typeof adapter === "function") {
- var Adapter = adapter;
- adapter = new Adapter(options);
- }
- return adapter;
+ // return the adapter as is as it's unusable otherwise
+ return adapter;
}
+
+export default loadAdapter;
diff --git a/src/Adapters/Email/MailAdapter.js b/src/Adapters/Email/MailAdapter.js
new file mode 100644
index 0000000000..82ea8b34c3
--- /dev/null
+++ b/src/Adapters/Email/MailAdapter.js
@@ -0,0 +1,23 @@
+
+/*
+ Mail Adapter prototype
+ A MailAdapter should implement at least sendMail()
+ */
+export class MailAdapter {
+ /*
+ * A method for sending mail
+ * @param options would have the parameters
+ * - to: the recipient
+ * - text: the raw text of the message
+ * - subject: the subject of the email
+ */
+ sendMail(options) {}
+
+ /* You can implement those methods if you want
+ * to provide HTML templates etc...
+ */
+ // sendVerificationEmail({ link, appName, user }) {}
+ // sendPasswordResetEmail({ link, appName, user }) {}
+}
+
+export default MailAdapter;
diff --git a/src/Adapters/Email/SimpleMailgunAdapter.js b/src/Adapters/Email/SimpleMailgunAdapter.js
new file mode 100644
index 0000000000..a90a43d77b
--- /dev/null
+++ b/src/Adapters/Email/SimpleMailgunAdapter.js
@@ -0,0 +1,32 @@
+import Mailgun from 'mailgun-js';
+
+let SimpleMailgunAdapter = mailgunOptions => {
+ if (!mailgunOptions || !mailgunOptions.apiKey || !mailgunOptions.domain) {
+ throw 'SimpleMailgunAdapter requires an API Key and domain.';
+ }
+ let mailgun = Mailgun(mailgunOptions);
+
+ let sendMail = ({to, subject, text}) => {
+ let data = {
+ from: mailgunOptions.fromAddress,
+ to: to,
+ subject: subject,
+ text: text,
+ }
+
+ return new Promise((resolve, reject) => {
+ mailgun.messages().send(data, (err, body) => {
+ if (typeof err !== 'undefined') {
+ reject(err);
+ }
+ resolve(body);
+ });
+ });
+ }
+
+ return Object.freeze({
+ sendMail: sendMail
+ });
+}
+
+module.exports = SimpleMailgunAdapter
diff --git a/src/Adapters/Files/S3Adapter.js b/src/Adapters/Files/S3Adapter.js
index 0732fbfeb9..d63880f461 100644
--- a/src/Adapters/Files/S3Adapter.js
+++ b/src/Adapters/Files/S3Adapter.js
@@ -4,23 +4,38 @@
import * as AWS from 'aws-sdk';
import { FilesAdapter } from './FilesAdapter';
+import requiredParameter from '../../requiredParameter';
const DEFAULT_S3_REGION = "us-east-1";
+function parseS3AdapterOptions(...options) {
+ if (options.length === 1 && typeof options[0] == "object") {
+ return options;
+ }
+
+ const additionalOptions = options[3] || {};
+
+ return {
+ accessKey: options[0],
+ secretKey: options[1],
+ bucket: options[2],
+ region: additionalOptions.region
+ }
+}
+
export class S3Adapter extends FilesAdapter {
// Creates an S3 session.
// Providing AWS access and secret keys is mandatory
// Region and bucket will use sane defaults if omitted
constructor(
- accessKey,
- secretKey,
- bucket,
- { region = DEFAULT_S3_REGION,
- bucketPrefix = '',
- directAccess = false } = {}
- ) {
+ accessKey = requiredParameter('S3Adapter requires an accessKey'),
+ secretKey = requiredParameter('S3Adapter requires a secretKey'),
+ bucket,
+ { region = DEFAULT_S3_REGION,
+ bucketPrefix = '',
+ directAccess = false } = {}) {
super();
-
+
this._region = region;
this._bucket = bucket;
this._bucketPrefix = bucketPrefix;
diff --git a/src/Adapters/Logger/FileLoggerAdapter.js b/src/Adapters/Logger/FileLoggerAdapter.js
index 9e3082424d..3d3c192f8f 100644
--- a/src/Adapters/Logger/FileLoggerAdapter.js
+++ b/src/Adapters/Logger/FileLoggerAdapter.js
@@ -101,7 +101,6 @@ let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => {
export class FileLoggerAdapter extends LoggerAdapter {
constructor(options = {}) {
super();
-
this._logsFolder = options.logsFolder || LOGS_FOLDER;
// check logs folder exists
diff --git a/src/Adapters/Push/OneSignalPushAdapter.js b/src/Adapters/Push/OneSignalPushAdapter.js
index fe2fcc0b84..b92d00c53e 100644
--- a/src/Adapters/Push/OneSignalPushAdapter.js
+++ b/src/Adapters/Push/OneSignalPushAdapter.js
@@ -18,6 +18,10 @@ export class OneSignalPushAdapter extends PushAdapter {
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
this.OneSignalConfig = {};
+ const { oneSignalAppId, oneSignalApiKey } = pushConfig;
+ if (!oneSignalAppId || !oneSignalApiKey) {
+ throw "Trying to initialize OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey";
+ }
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
diff --git a/src/Config.js b/src/Config.js
index 988efb1e11..8042d6db4d 100644
--- a/src/Config.js
+++ b/src/Config.js
@@ -23,15 +23,63 @@ export class Config {
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
+
+ this.serverURL = cacheInfo.serverURL;
+ this.publicServerURL = cacheInfo.publicServerURL;
+ this.verifyUserEmails = cacheInfo.verifyUserEmails;
+ this.appName = cacheInfo.appName;
+
this.hooksController = cacheInfo.hooksController;
this.filesController = cacheInfo.filesController;
- this.pushController = cacheInfo.pushController;
+ this.pushController = cacheInfo.pushController;
this.loggerController = cacheInfo.loggerController;
+ this.userController = cacheInfo.userController;
this.oauth = cacheInfo.oauth;
-
+ this.customPages = cacheInfo.customPages || {};
this.mount = mount;
}
-}
+
+ static validate(options) {
+ this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
+ appName: options.appName,
+ publicServerURL: options.publicServerURL})
+ }
+
+ static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
+ if (verifyUserEmails) {
+ if (typeof appName !== 'string') {
+ throw 'An app name is required when using email verification.';
+ }
+ if (typeof publicServerURL !== 'string') {
+ throw 'A public server url is required when using email verification.';
+ }
+ }
+ }
+
+ get invalidLinkURL() {
+ return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
+ }
+
+ get verifyEmailSuccessURL() {
+ return this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html`;
+ }
+
+ get choosePasswordURL() {
+ return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`;
+ }
+
+ get requestResetPasswordURL() {
+ return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`;
+ }
+
+ get passwordResetSuccessURL() {
+ return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`;
+ }
+
+ get verifyEmailURL() {
+ return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
+ }
+};
export default Config;
module.exports = Config;
diff --git a/src/Controllers/AdaptableController.js b/src/Controllers/AdaptableController.js
index ef45b0225f..902a6eb349 100644
--- a/src/Controllers/AdaptableController.js
+++ b/src/Controllers/AdaptableController.js
@@ -10,10 +10,13 @@ based on the parameters passed
// _adapter is private, use Symbol
var _adapter = Symbol();
+import Config from '../Config';
export class AdaptableController {
- constructor(adapter) {
+ constructor(adapter, appId, options) {
+ this.options = options;
+ this.appId = appId;
this.adapter = adapter;
}
@@ -26,12 +29,15 @@ export class AdaptableController {
return this[_adapter];
}
+ get config() {
+ return new Config(this.appId);
+ }
+
expectedAdapterType() {
throw new Error("Subclasses should implement expectedAdapterType()");
}
validateAdapter(adapter) {
-
if (!adapter) {
throw new Error(this.constructor.name+" requires an adapter");
}
@@ -56,8 +62,7 @@ export class AdaptableController {
}, {});
if (Object.keys(mismatches).length > 0) {
- console.error(adapter, mismatches);
- throw new Error("Adapter prototype don't match expected prototype");
+ throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
}
}
}
diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js
new file mode 100644
index 0000000000..35da9a1f70
--- /dev/null
+++ b/src/Controllers/UserController.js
@@ -0,0 +1,218 @@
+import { randomString } from '../cryptoUtils';
+import { inflate } from '../triggers';
+import AdaptableController from './AdaptableController';
+import MailAdapter from '../Adapters/Email/MailAdapter';
+
+var DatabaseAdapter = require('../DatabaseAdapter');
+var RestWrite = require('../RestWrite');
+var RestQuery = require('../RestQuery');
+var hash = require('../password').hash;
+var Auth = require('../Auth');
+
+export class UserController extends AdaptableController {
+
+ constructor(adapter, appId, options = {}) {
+ super(adapter, appId, options);
+ }
+
+ validateAdapter(adapter) {
+ // Allow no adapter
+ if (!adapter && !this.shouldVerifyEmails) {
+ return;
+ }
+ super.validateAdapter(adapter);
+ }
+
+ expectedAdapterType() {
+ return MailAdapter;
+ }
+
+ get shouldVerifyEmails() {
+ return this.options.verifyUserEmails;
+ }
+
+ setEmailVerifyToken(user) {
+ if (this.shouldVerifyEmails) {
+ user._email_verify_token = randomString(25);
+ user.emailVerified = false;
+ }
+ }
+
+
+ verifyEmail(username, token) {
+
+ return new Promise((resolve, reject) => {
+
+ // Trying to verify email when not enabled
+ if (!this.shouldVerifyEmails) {
+ reject();
+ return;
+ }
+
+ var database = this.config.database;
+
+ database.collection('_User').then(coll => {
+ // Need direct database access because verification token is not a parse field
+ return coll.findAndModify({
+ username: username,
+ _email_verify_token: token,
+ }, null, {$set: {emailVerified: true}}, (err, doc) => {
+ if (err || !doc.value) {
+ reject(err);
+ } else {
+ resolve(doc.value);
+ }
+ });
+ });
+
+ });
+ }
+
+ checkResetTokenValidity(username, token) {
+ return new Promise((resolve, reject) => {
+ return this.config.database.collection('_User').then(coll => {
+ return coll.findOne({
+ username: username,
+ _perishable_token: token,
+ }, (err, doc) => {
+ if (err || !doc) {
+ reject(err);
+ } else {
+ resolve(doc);
+ }
+ });
+ });
+ });
+ }
+
+ getUserIfNeeded(user) {
+ if (user.username && user.email) {
+ return Promise.resolve(user);
+ }
+ var where = {};
+ if (user.username) {
+ where.username = user.username;
+ }
+ if (user.email) {
+ where.email = user.email;
+ }
+
+ var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
+ return query.execute().then(function(result){
+ if (result.results.length != 1) {
+ return Promise.reject();
+ }
+ return result.results[0];
+ })
+ }
+
+
+ sendVerificationEmail(user) {
+ if (!this.shouldVerifyEmails) {
+ return;
+ }
+ // We may need to fetch the user in case of update email
+ this.getUserIfNeeded(user).then((user) => {
+ const token = encodeURIComponent(user._email_verify_token);
+ const username = encodeURIComponent(user.username);
+ let link = `${this.config.verifyEmailURL}?token=${token}&username=${username}`;
+ let options = {
+ appName: this.config.appName,
+ link: link,
+ user: inflate('_User', user),
+ };
+ if (this.adapter.sendVerificationEmail) {
+ this.adapter.sendVerificationEmail(options);
+ } else {
+ this.adapter.sendMail(this.defaultVerificationEmail(options));
+ }
+ });
+ }
+
+ setPasswordResetToken(email) {
+ var database = this.config.database;
+ var token = randomString(25);
+ return new Promise((resolve, reject) => {
+ return database.collection('_User').then(coll => {
+ // Need direct database access because verification token is not a parse field
+ return coll.findAndModify({
+ email: email,
+ }, null, {$set: {_perishable_token: token}}, (err, doc) => {
+ if (err || !doc.value) {
+ console.error(err);
+ reject(err);
+ } else {
+ doc.value._perishable_token = token;
+ resolve(doc.value);
+ }
+ });
+ });
+ });
+ }
+
+ sendPasswordResetEmail(email) {
+ if (!this.adapter) {
+ throw "Trying to send a reset password but no adapter is set";
+ // TODO: No adapter?
+ return;
+ }
+
+ return this.setPasswordResetToken(email).then((user) => {
+
+ const token = encodeURIComponent(user._perishable_token);
+ const username = encodeURIComponent(user.username);
+ let link = `${this.config.requestResetPasswordURL}?token=${token}&username=${username}`
+
+ let options = {
+ appName: this.config.appName,
+ link: link,
+ user: inflate('_User', user),
+ };
+
+ if (this.adapter.sendPasswordResetEmail) {
+ this.adapter.sendPasswordResetEmail(options);
+ } else {
+ this.adapter.sendMail(this.defaultResetPasswordEmail(options));
+ }
+
+ return Promise.resolve(user);
+ });
+ }
+
+ updatePassword(username, token, password, config) {
+ return this.checkResetTokenValidity(username, token).then(() => {
+ return updateUserPassword(username, token, password, this.config);
+ });
+ }
+
+ defaultVerificationEmail({link, user, appName, }) {
+ let text = "Hi,\n\n" +
+ "You are being asked to confirm the e-mail address " + user.email + " with " + appName + "\n\n" +
+ "" +
+ "Click here to confirm it:\n" + link;
+ let to = user.get("email");
+ let subject = 'Please verify your e-mail for ' + appName;
+ return { text, to, subject };
+ }
+
+ defaultResetPasswordEmail({link, user, appName, }) {
+ let text = "Hi,\n\n" +
+ "You requested to reset your password for " + appName + ".\n\n" +
+ "" +
+ "Click here to reset it:\n" + link;
+ let to = user.get("email");
+ let subject = 'Password Reset for ' + appName;
+ return { text, to, subject };
+ }
+}
+
+// Mark this private
+function updateUserPassword(username, token, password, config) {
+ var write = new RestWrite(config, Auth.master(config), '_User', {
+ username: username,
+ _perishable_token: token
+ }, {password: password, _perishable_token: null }, undefined);
+ return write.execute();
+ }
+
+export default UserController;
diff --git a/src/PromiseRouter.js b/src/PromiseRouter.js
index 8155c7967b..4070f70619 100644
--- a/src/PromiseRouter.js
+++ b/src/PromiseRouter.js
@@ -5,6 +5,8 @@
// themselves use our routing information, without disturbing express
// components that external developers may be modifying.
+import express from 'express';
+
export default class PromiseRouter {
// Each entry should be an object with:
// path: the path to route, in express format
@@ -15,8 +17,8 @@ export default class PromiseRouter {
// status: optional. the http status code. defaults to 200
// response: a json object with the content of the response
// location: optional. a location header
- constructor() {
- this.routes = [];
+ constructor(routes = []) {
+ this.routes = routes;
this.mountRoutes();
}
@@ -125,6 +127,29 @@ export default class PromiseRouter {
}
}
};
+
+ expressApp() {
+ var expressApp = express();
+ for (var route of this.routes) {
+ switch(route.method) {
+ case 'POST':
+ expressApp.post(route.path, makeExpressHandler(route.handler));
+ break;
+ case 'GET':
+ expressApp.get(route.path, makeExpressHandler(route.handler));
+ break;
+ case 'PUT':
+ expressApp.put(route.path, makeExpressHandler(route.handler));
+ break;
+ case 'DELETE':
+ expressApp.delete(route.path, makeExpressHandler(route.handler));
+ break;
+ default:
+ throw 'unexpected code branch';
+ }
+ }
+ return expressApp;
+ }
}
// Global flag. Set this to true to log every request and response.
@@ -142,15 +167,24 @@ function makeExpressHandler(promiseHandler) {
JSON.stringify(req.body, null, 2));
}
promiseHandler(req).then((result) => {
- if (!result.response) {
- console.log('BUG: the handler did not include a "response" field');
+ if (!result.response && !result.location && !result.text) {
+ console.log('BUG: the handler did not include a "response" or a "location" field');
throw 'control should not get here';
}
if (PromiseRouter.verbose) {
- console.log('response:', JSON.stringify(result.response, null, 2));
+ console.log('response:', JSON.stringify(result, null, 2));
}
+
var status = result.status || 200;
res.status(status);
+
+ if (result.text) {
+ return res.send(result.text);
+ }
+
+ if (result.location && !result.response) {
+ return res.redirect(result.location);
+ }
if (result.location) {
res.set('Location', result.location);
}
diff --git a/src/RestWrite.js b/src/RestWrite.js
index 66ea69ff64..02815403ce 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -465,12 +465,18 @@ RestWrite.prototype.transformUser = function() {
'address');
}
return Promise.resolve();
- });
+ }).then(() => {
+ // We updated the email, send a new validation
+ this.storage['sendVerificationEmail'] = true;
+ this.config.userController.setEmailVerifyToken(this.data);
+ return Promise.resolve();
+ })
});
};
// Handles any followup logic
RestWrite.prototype.handleFollowup = function() {
+
if (this.storage && this.storage['clearSessions']) {
var sessionQuery = {
user: {
@@ -480,9 +486,16 @@ RestWrite.prototype.handleFollowup = function() {
}
};
delete this.storage['clearSessions'];
- return this.config.database.destroy('_Session', sessionQuery)
+ this.config.database.destroy('_Session', sessionQuery)
.then(this.handleFollowup.bind(this));
}
+
+ if (this.storage && this.storage['sendVerificationEmail']) {
+ delete this.storage['sendVerificationEmail'];
+ // Fire and forget!
+ this.config.userController.sendVerificationEmail(this.data);
+ this.handleFollowup.bind(this);
+ }
};
// Handles the _Role class specialness.
@@ -832,4 +845,5 @@ RestWrite.prototype.objectId = function() {
return this.data.objectId || this.query.objectId;
};
+export default RestWrite;
module.exports = RestWrite;
diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js
new file mode 100644
index 0000000000..1fbde2d531
--- /dev/null
+++ b/src/Routers/GlobalConfigRouter.js
@@ -0,0 +1,48 @@
+// global_config.js
+
+var Parse = require('parse/node').Parse;
+
+import PromiseRouter from '../PromiseRouter';
+
+export class GlobalConfigRouter extends PromiseRouter {
+ getGlobalConfig(req) {
+ return req.config.database.rawCollection('_GlobalConfig')
+ .then(coll => coll.findOne({'_id': 1}))
+ .then(globalConfig => ({response: { params: globalConfig.params }}))
+ .catch(() => ({
+ status: 404,
+ response: {
+ code: Parse.Error.INVALID_KEY_NAME,
+ error: 'config does not exist',
+ }
+ }));
+ }
+ updateGlobalConfig(req) {
+ if (!req.auth.isMaster) {
+ return Promise.resolve({
+ status: 401,
+ response: {error: 'unauthorized'},
+ });
+ }
+
+ return req.config.database.rawCollection('_GlobalConfig')
+ .then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body }))
+ .then(response => {
+ return { response: { result: true } }
+ })
+ .catch(() => ({
+ status: 404,
+ response: {
+ code: Parse.Error.INVALID_KEY_NAME,
+ error: 'config cannot be updated',
+ }
+ }));
+ }
+
+ mountRoutes() {
+ this.route('GET', '/config', req => { return this.getGlobalConfig(req) });
+ this.route('PUT', '/config', req => { return this.updateGlobalConfig(req) });
+ }
+}
+
+export default GlobalConfigRouter;
diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js
new file mode 100644
index 0000000000..017caef395
--- /dev/null
+++ b/src/Routers/PublicAPIRouter.js
@@ -0,0 +1,159 @@
+import PromiseRouter from '../PromiseRouter';
+import UserController from '../Controllers/UserController';
+import Config from '../Config';
+import express from 'express';
+import path from 'path';
+import fs from 'fs';
+
+let public_html = path.resolve(__dirname, "../../public_html");
+let views = path.resolve(__dirname, '../../views');
+
+export class PublicAPIRouter extends PromiseRouter {
+
+ verifyEmail(req) {
+ let { token, username }= req.query;
+ let appId = req.params.appId;
+ let config = new Config(appId);
+
+ if (!config.publicServerURL) {
+ return this.missingPublicServerURL();
+ }
+
+ if (!token || !username) {
+ return this.invalidLink(req);
+ }
+
+ let userController = config.userController;
+ return userController.verifyEmail(username, token).then( () => {
+ return Promise.resolve({
+ status: 302,
+ location: `${config.verifyEmailSuccessURL}?username=${username}`
+ });
+ }, ()=> {
+ return this.invalidLink(req);
+ })
+ }
+
+ changePassword(req) {
+ return new Promise((resolve, reject) => {
+ let config = new Config(req.query.id);
+ if (!config.publicServerURL) {
+ return resolve({
+ status: 404,
+ text: 'Not found.'
+ });
+ }
+ // Should we keep the file in memory or leave like that?
+ fs.readFile(path.resolve(views, "choose_password"), 'utf-8', (err, data) => {
+ if (err) {
+ return reject(err);
+ }
+ data = data.replace("PARSE_SERVER_URL", `'${config.publicServerURL}'`);
+ resolve({
+ text: data
+ })
+ });
+ });
+ }
+
+ requestResetPassword(req) {
+
+ let config = req.config;
+
+ if (!config.publicServerURL) {
+ return this.missingPublicServerURL();
+ }
+
+ let { username, token } = req.query;
+
+ if (!username || !token) {
+ return this.invalidLink(req);
+ }
+
+ return config.userController.checkResetTokenValidity(username, token).then( (user) => {
+ return Promise.resolve({
+ status: 302,
+ location: `${config.choosePasswordURL}?token=${token}&id=${config.applicationId}&username=${username}&app=${config.appName}`
+ })
+ }, () => {
+ return this.invalidLink(req);
+ })
+ }
+
+ resetPassword(req) {
+
+ let config = req.config;
+
+ if (!config.publicServerURL) {
+ return this.missingPublicServerURL();
+ }
+
+ let {
+ username,
+ token,
+ new_password
+ } = req.body;
+
+ if (!username || !token || !new_password) {
+ return this.invalidLink(req);
+ }
+
+ return config.userController.updatePassword(username, token, new_password).then((result) => {
+ return Promise.resolve({
+ status: 302,
+ location: config.passwordResetSuccessURL
+ });
+ }, (err) => {
+ return Promise.resolve({
+ status: 302,
+ location: `${config.choosePasswordURL}?token=${token}&id=${config.applicationId}&username=${username}&error=${err}&app=${config.appName}`
+ });
+ });
+
+ }
+
+ invalidLink(req) {
+ return Promise.resolve({
+ status: 302,
+ location: req.config.invalidLinkURL
+ });
+ }
+
+ missingPublicServerURL() {
+ return Promise.resolve({
+ text: 'Not found.',
+ status: 404
+ });
+ }
+
+ setConfig(req) {
+ req.config = new Config(req.params.appId);
+ return Promise.resolve();
+ }
+
+ mountRoutes() {
+ this.route('GET','/apps/:appId/verify_email',
+ req => { this.setConfig(req) },
+ req => { return this.verifyEmail(req); });
+
+ this.route('GET','/apps/choose_password',
+ req => { return this.changePassword(req); });
+
+ this.route('POST','/apps/:appId/request_password_reset',
+ req => { this.setConfig(req) },
+ req => { return this.resetPassword(req); });
+
+ this.route('GET','/apps/:appId/request_password_reset',
+ req => { this.setConfig(req) },
+ req => { return this.requestResetPassword(req); });
+ }
+
+ expressApp() {
+ let router = express();
+ router.use("/apps", express.static(public_html));
+ router.use("/", super.expressApp());
+ return router;
+ }
+}
+
+export default PublicAPIRouter;
diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js
index 4cba3edb0d..21dc80ba3f 100644
--- a/src/Routers/UsersRouter.js
+++ b/src/Routers/UsersRouter.js
@@ -1,14 +1,15 @@
// These methods handle the User-related routes.
-import deepcopy from 'deepcopy';
+import deepcopy from 'deepcopy';
-import ClassesRouter from './ClassesRouter';
-import PromiseRouter from '../PromiseRouter';
-import rest from '../rest';
-import Auth from '../Auth';
+import ClassesRouter from './ClassesRouter';
+import PromiseRouter from '../PromiseRouter';
+import rest from '../rest';
+import Auth from '../Auth';
import passwordCrypto from '../password';
-import RestWrite from '../RestWrite';
-import { newToken } from '../cryptoUtils';
+import RestWrite from '../RestWrite';
+let cryptoUtils = require('../cryptoUtils');
+let triggers = require('../triggers');
export class UsersRouter extends ClassesRouter {
handleFind(req) {
@@ -25,7 +26,18 @@ export class UsersRouter extends ClassesRouter {
let data = deepcopy(req.body);
req.body = data;
req.params.className = '_User';
+
+ //req.config.userController.setEmailVerifyToken(req.body);
+
return super.handleCreate(req);
+
+ // if (req.config.verifyUserEmails) {
+ // // Send email as fire-and-forget once the user makes it into the DB.
+ // p.then(() => {
+ // req.config.userController.sendVerificationEmail(req.body);
+ // });
+ // }
+ // return p;
}
handleUpdate(req) {
@@ -87,7 +99,7 @@ export class UsersRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
}
- let token = 'r:' + newToken();
+ let token = 'r:' + cryptoUtils.newToken();
user.sessionToken = token;
delete user.password;
@@ -140,6 +152,23 @@ export class UsersRouter extends ClassesRouter {
}
return Promise.resolve(success);
}
+
+ handleResetRequest(req) {
+ let { email } = req.body;
+ if (!email) {
+ throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email");
+ }
+ let userController = req.config.userController;
+
+ return userController.sendPasswordResetEmail(email).then((token) => {
+ return Promise.resolve({
+ response: {}
+ });
+ }, (err) => {
+ throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `no user found with email ${email}`);
+ });
+ }
+
mountRoutes() {
this.route('GET', '/users', req => { return this.handleFind(req); });
@@ -150,9 +179,7 @@ export class UsersRouter extends ClassesRouter {
this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
this.route('GET', '/login', req => { return this.handleLogIn(req); });
this.route('POST', '/logout', req => { return this.handleLogOut(req); });
- this.route('POST', '/requestPasswordReset', () => {
- throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'This path is not implemented yet.');
- });
+ this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); })
}
}
diff --git a/src/global_config.js b/src/global_config.js
deleted file mode 100644
index 0c005e4dc6..0000000000
--- a/src/global_config.js
+++ /dev/null
@@ -1,46 +0,0 @@
-// global_config.js
-
-var Parse = require('parse/node').Parse;
-
-import PromiseRouter from './PromiseRouter';
-var router = new PromiseRouter();
-
-function getGlobalConfig(req) {
- return req.config.database.rawCollection('_GlobalConfig')
- .then(coll => coll.findOne({'_id': 1}))
- .then(globalConfig => ({response: { params: globalConfig.params }}))
- .catch(() => ({
- status: 404,
- response: {
- code: Parse.Error.INVALID_KEY_NAME,
- error: 'config does not exist',
- }
- }));
-}
-
-function updateGlobalConfig(req) {
- if (!req.auth.isMaster) {
- return Promise.resolve({
- status: 401,
- response: {error: 'unauthorized'},
- });
- }
-
- return req.config.database.rawCollection('_GlobalConfig')
- .then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body }))
- .then(response => {
- return { response: { result: true } }
- })
- .catch(() => ({
- status: 404,
- response: {
- code: Parse.Error.INVALID_KEY_NAME,
- error: 'config cannot be updated',
- }
- }));
-}
-
-router.route('GET', '/config', getGlobalConfig);
-router.route('PUT', '/config', updateGlobalConfig);
-
-module.exports = router;
diff --git a/src/index.js b/src/index.js
index 5062b6b03a..4ee5d14074 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,32 +11,36 @@ var batch = require('./batch'),
Parse = require('parse/node').Parse;
import cache from './cache';
-import PromiseRouter from './PromiseRouter';
-import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
-import { S3Adapter } from './Adapters/Files/S3Adapter';
-import { FilesController } from './Controllers/FilesController';
+import Config from './Config';
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
-import { PushController } from './Controllers/PushController';
-
-import { ClassesRouter } from './Routers/ClassesRouter';
-import { InstallationsRouter } from './Routers/InstallationsRouter';
-import { UsersRouter } from './Routers/UsersRouter';
-import { SessionsRouter } from './Routers/SessionsRouter';
-import { RolesRouter } from './Routers/RolesRouter';
+//import passwordReset from './passwordReset';
+import PromiseRouter from './PromiseRouter';
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
+import { ClassesRouter } from './Routers/ClassesRouter';
+import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
+import { FilesController } from './Controllers/FilesController';
+import { FilesRouter } from './Routers/FilesRouter';
import { FunctionsRouter } from './Routers/FunctionsRouter';
-import { SchemasRouter } from './Routers/SchemasRouter';
+import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
-import { PushRouter } from './Routers/PushRouter';
-import { FilesRouter } from './Routers/FilesRouter';
import { LogsRouter } from './Routers/LogsRouter';
import { HooksRouter } from './Routers/HooksRouter';
+import { PublicAPIRouter } from './Routers/PublicAPIRouter';
+import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
+import { HooksController } from './Controllers/HooksController';
+import { UserController } from './Controllers/UserController';
+import { InstallationsRouter } from './Routers/InstallationsRouter';
import { loadAdapter } from './Adapters/AdapterLoader';
-import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { LoggerController } from './Controllers/LoggerController';
-import { HooksController } from './Controllers/HooksController';
+import { PushController } from './Controllers/PushController';
+import { PushRouter } from './Routers/PushRouter';
+import { RolesRouter } from './Routers/RolesRouter';
+import { S3Adapter } from './Adapters/Files/S3Adapter';
+import { SchemasRouter } from './Routers/SchemasRouter';
+import { SessionsRouter } from './Routers/SessionsRouter';
+import { UsersRouter } from './Routers/UsersRouter';
import requiredParameter from './requiredParameter';
import { randomString } from './cryptoUtils';
@@ -72,6 +76,7 @@ addParseCloud();
function ParseServer({
appId = requiredParameter('You must provide an appId!'),
masterKey = requiredParameter('You must provide a masterKey!'),
+ appName,
databaseAdapter,
filesAdapter,
push,
@@ -89,7 +94,16 @@ function ParseServer({
allowClientClassCreation = true,
oauth = {},
serverURL = requiredParameter('You must provide a serverURL!'),
- maxUploadSize = '20mb'
+ maxUploadSize = '20mb',
+ verifyUserEmails = false,
+ emailAdapter,
+ publicServerURL,
+ customPages = {
+ invalidLink: undefined,
+ verifyEmailSuccess: undefined,
+ choosePassword: undefined,
+ passwordResetSuccess: undefined
+ },
}) {
// Initialize the node client SDK automatically
@@ -103,7 +117,7 @@ function ParseServer({
if (databaseURI) {
DatabaseAdapter.setAppDatabaseURI(appId, databaseURI);
}
-
+
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
@@ -118,16 +132,19 @@ function ParseServer({
const filesControllerAdapter = loadAdapter(filesAdapter, GridStoreAdapter);
const pushControllerAdapter = loadAdapter(push, ParsePushAdapter);
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
-
+ const emailControllerAdapter = loadAdapter(emailAdapter);
// We pass the options and the base class for the adatper,
// Note that passing an instance would work too
- const filesController = new FilesController(filesControllerAdapter);
- const pushController = new PushController(pushControllerAdapter);
- const loggerController = new LoggerController(loggerControllerAdapter);
+ const filesController = new FilesController(filesControllerAdapter, appId);
+ const pushController = new PushController(pushControllerAdapter, appId);
+ const loggerController = new LoggerController(loggerControllerAdapter, appId);
const hooksController = new HooksController(appId, collectionPrefix);
+ const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
+
cache.apps.set(appId, {
masterKey: masterKey,
+ serverURL: serverURL,
collectionPrefix: collectionPrefix,
clientKey: clientKey,
javascriptKey: javascriptKey,
@@ -139,25 +156,34 @@ function ParseServer({
pushController: pushController,
loggerController: loggerController,
hooksController: hooksController,
+ userController: userController,
+ verifyUserEmails: verifyUserEmails,
enableAnonymousUsers: enableAnonymousUsers,
allowClientClassCreation: allowClientClassCreation,
- oauth: oauth
+ oauth: oauth,
+ appName: appName,
+ publicServerURL: publicServerURL,
+ customPages: customPages,
});
- // To maintain compatibility. TODO: Remove in v2.1
+ // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
if (process.env.FACEBOOK_APP_ID) {
cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
}
+
+ Config.validate(cache.apps.get(appId));
// This app serves the Parse API directly.
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
var api = express();
-
+ //api.use("/apps", express.static(__dirname + "/public"));
// File handling needs to be before default middlewares are applied
api.use('/', new FilesRouter().getExpressRouter({
maxUploadSize: maxUploadSize
}));
+ api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressApp());
+
// TODO: separate this from the regular ParseServer object
if (process.env.TESTING == 1) {
api.use('/', require('./testing-routes').router);
@@ -183,20 +209,22 @@ function ParseServer({
];
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
- routers.push(require('./global_config'));
+ routers.push(new GlobalConfigRouter());
}
if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) {
routers.push(new HooksRouter());
}
+
+ let routes = routers.reduce((memo, router) => {
+ return memo.concat(router.routes);
+ }, []);
- let appRouter = new PromiseRouter();
- routers.forEach((router) => {
- appRouter.merge(router);
- });
+ let appRouter = new PromiseRouter(routes);
+
batch.mountOnto(appRouter);
- appRouter.mountOnto(api);
+ api.use(appRouter.expressApp());
api.use(middlewares.handleParseErrors);
@@ -222,5 +250,5 @@ function addParseCloud() {
module.exports = {
ParseServer: ParseServer,
- S3Adapter: S3Adapter
+ S3Adapter: S3Adapter,
};
diff --git a/src/middlewares.js b/src/middlewares.js
index b9a8d6ec4b..8489cda02d 100644
--- a/src/middlewares.js
+++ b/src/middlewares.js
@@ -174,6 +174,9 @@ var handleParseErrors = function(err, req, res, next) {
res.status(httpStatus);
res.json({code: err.code, error: err.message});
+ } else if (err.status && err.message) {
+ res.status(err.status);
+ res.json({error: err.message});
} else {
console.log('Uncaught internal server error.', err, err.stack);
res.status(500);
diff --git a/src/transform.js b/src/transform.js
index f254f0d464..8829f394ae 100644
--- a/src/transform.js
+++ b/src/transform.js
@@ -42,6 +42,12 @@ export function transformKeyValue(schema, className, restKey, restValue, options
key = '_updated_at';
timeField = true;
break;
+ case '_email_verify_token':
+ key = "_email_verify_token";
+ break;
+ case '_perishable_token':
+ key = "_perishable_token";
+ break;
case 'sessionToken':
case '_session_token':
key = '_session_token';
@@ -649,7 +655,7 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
restObject['authData'][provider] = mongoObject[key];
break;
}
-
+
if (key.indexOf('_p_') == 0) {
var newKey = key.substring(3);
var expected;
diff --git a/views/choose_password b/views/choose_password
new file mode 100644
index 0000000000..097cbd2077
--- /dev/null
+++ b/views/choose_password
@@ -0,0 +1,176 @@
+
+
+
+
+ Password Reset
+
+
+
+ Reset Your Password
+
+
+
+
+
+