diff --git a/package.json b/package.json index cc2ff211ce..49f2808c21 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "body-parser": "^1.14.2", "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/spec/MockEmailAdapter.js b/spec/MockEmailAdapter.js new file mode 100644 index 0000000000..e06e27cb08 --- /dev/null +++ b/spec/MockEmailAdapter.js @@ -0,0 +1,3 @@ +module.exports = { + sendVerificationEmail: () => Promise.resolve(); +} diff --git a/spec/MockEmailAdapterWithOptions.js b/spec/MockEmailAdapterWithOptions.js new file mode 100644 index 0000000000..fe402e0650 --- /dev/null +++ b/spec/MockEmailAdapterWithOptions.js @@ -0,0 +1,8 @@ +module.exports = options => { + if (!options) { + throw "Options were not provided" + } + return { + sendVerificationEmail: () => Promise.resolve() + } +} diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 33da62e824..35c9cc6871 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -47,6 +47,217 @@ describe('Parse.User testing', () => { }); }); + it('sends verification email if email verification is enabled', done => { + var emailAdapter = { + sendVerificationEmail: () => 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, + }); + spyOn(emailAdapter, 'sendVerificationEmail'); + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + 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 if email verification is disabled', done => { + var emailAdapter = { + sendVerificationEmail: () => 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(); + } + } + 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, + }); + 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/verify_email_success.html?username=zxcv'); + user.fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(true); + 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: emailAdapter, + }); + 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 => { + request.get('http://localhost:8378/1/verify_email', { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/invalid_link.html'); + done() + }); + }); + + it('redirects you to invalid link if you try to validate a nonexistant users email', done => { + request.get('http://localhost:8378/1/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/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/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/invalid_link.html'); + user.fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(false); + 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: emailAdapter, + }); + 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("user login wrong username", (done) => { Parse.User.signUp("asdf", "zxcv", null, { success: function(user) { @@ -1628,7 +1839,7 @@ describe('Parse.User testing', () => { done(); }); }); - + it('test parse user become', (done) => { var sessionToken = null; Parse.Promise.as().then(function() { diff --git a/spec/index.spec.js b/spec/index.spec.js index 0e3eba5db8..c4280128c0 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 => { @@ -36,4 +37,114 @@ 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', + }), + }); + 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', + } + }, + }); + 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', + } + }, + }); + 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', + })).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', + } + }, + })).toThrow('SimpleMailgunAdapter requires an API Key and domain.'); + done(); + }); }); diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js index cfe51ffd7e..1557324b31 100644 --- a/src/Adapters/AdapterLoader.js +++ b/src/Adapters/AdapterLoader.js @@ -1,4 +1,3 @@ - export function loadAdapter(options, defaultAdapter) { let adapter; @@ -12,7 +11,7 @@ export function loadAdapter(options, defaultAdapter) { adapter = options.adapter; } } - + if (!adapter) { adapter = defaultAdapter; } @@ -26,10 +25,12 @@ export function loadAdapter(options, defaultAdapter) { } } // From there it's either a function or an object - // if it's an function, instanciate and pass the options + // if it's an function, instanciate and pass the options if (typeof adapter === "function") { var Adapter = adapter; adapter = new Adapter(options); } return adapter; } + +module.exports = { loadAdapter } diff --git a/src/Adapters/Email/SimpleMailgunAdapter.js b/src/Adapters/Email/SimpleMailgunAdapter.js new file mode 100644 index 0000000000..2d51173d84 --- /dev/null +++ b/src/Adapters/Email/SimpleMailgunAdapter.js @@ -0,0 +1,39 @@ +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 { + sendVerificationEmail: ({ link, user, appName, }) => { + let verifyMessage = + "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; + return sendMail(user.email, 'Please verify your e-mail for ' + appName, verifyMessage); + } + } +} + +module.exports = SimpleMailgunAdapter diff --git a/src/Adapters/loadAdapter.js b/src/Adapters/loadAdapter.js new file mode 100644 index 0000000000..2ab7b35077 --- /dev/null +++ b/src/Adapters/loadAdapter.js @@ -0,0 +1,25 @@ +export default options => { + if (!options) { + return undefined; + } + + if (typeof options === 'string') { + //Configuring via module name with no options + return require(options)(); + } + + if (!options.module && !options.class) { + //Configuring via object + return options; + } + + if (options.module) { + //Configuring via module name + options + return require(options.module)(options.options) + } + + if (options.class) { + //Configuring via class + options + return options.class(options.options); + } +} diff --git a/src/Config.js b/src/Config.js index 4859c09c85..770500787d 100644 --- a/src/Config.js +++ b/src/Config.js @@ -24,9 +24,13 @@ export class Config { this.facebookAppIds = cacheInfo.facebookAppIds; this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers; + this.verifyUserEmails = cacheInfo.verifyUserEmails; + this.emailAdapter = cacheInfo.emailAdapter; + this.appName = cacheInfo.appName; + this.database = DatabaseAdapter.getDatabaseConnection(applicationId); this.filesController = cacheInfo.filesController; - this.pushController = cacheInfo.pushController; + this.pushController = cacheInfo.pushController; this.loggerController = cacheInfo.loggerController; this.oauth = cacheInfo.oauth; diff --git a/src/RestQuery.js b/src/RestQuery.js index b5bec1fbb6..15c199aac7 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -381,7 +381,7 @@ RestQuery.prototype.handleInclude = function() { this.include = this.include.slice(1); return this.handleInclude(); } - + return pathResponse; }; diff --git a/src/RestWrite.js b/src/RestWrite.js index 2728a9a6bd..02262b5ebe 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -86,21 +86,15 @@ RestWrite.prototype.execute = function() { // Uses the Auth object to get the list of roles, adds the user id RestWrite.prototype.getUserAndRoleACL = function() { - if (this.auth.isMaster) { + if (this.auth.isMaster || !this.auth.user) { return Promise.resolve(); } - - this.runOptions.acl = ['*']; - - if (this.auth.user) { - return this.auth.getUserRoles().then((roles) => { - roles.push(this.auth.user.id); - this.runOptions.acl = this.runOptions.acl.concat(roles); - return Promise.resolve(); - }); - }else{ + return this.auth.getUserRoles().then((roles) => { + roles.push('*'); + roles.push(this.auth.user.id); + this.runOptions.acl = roles; return Promise.resolve(); - } + }); }; // Validates this operation against the schema. @@ -176,11 +170,11 @@ RestWrite.prototype.validateAuthData = function() { var authData = this.data.authData; var anonData = this.data.authData.anonymous; - + if (this.config.enableAnonymousUsers === true && (anonData === null || (anonData && anonData.id))) { return this.handleAnonymousAuthData(); - } + } // Not anon, try other providers var providers = Object.keys(authData); @@ -710,7 +704,7 @@ RestWrite.prototype.runDatabaseOperation = function() { throw new Parse.Error(Parse.Error.SESSION_MISSING, 'cannot modify user ' + this.query.objectId); } - + if (this.className === '_Product' && this.data.download) { this.data.downloadName = this.data.download.name; } @@ -735,7 +729,7 @@ RestWrite.prototype.runDatabaseOperation = function() { ACL[this.data.objectId] = { read: true, write: true }; ACL['*'] = { read: true, write: false }; this.data.ACL = ACL; - } + } // Run a create return this.config.database.create(this.className, this.data, this.runOptions) .then(() => { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 29d33ed2ff..2610fc7651 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,26 @@ export class UsersRouter extends ClassesRouter { let data = deepcopy(req.body); req.body = data; req.params.className = '_User'; - return super.handleCreate(req); + + if (req.config.verifyUserEmails) { + req.body._email_verify_token = cryptoUtils.randomString(25); + req.body.emailVerified = false; + } + + let p = super.handleCreate(req); + + if (req.config.verifyUserEmails) { + // Send email as fire-and-forget once the user makes it into the DB. + p.then(() => { + let link = req.config.mount + "/verify_email?token=" + encodeURIComponent(req.body._email_verify_token) + "&username=" + encodeURIComponent(req.body.username); + req.config.emailAdapter.sendVerificationEmail({ + appName: req.config.appName, + link: link, + user: triggers.inflate('_User', req.body), + }); + }); + } + return p; } handleUpdate(req) { @@ -84,7 +104,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; @@ -150,6 +170,7 @@ export class UsersRouter extends ClassesRouter { this.route('POST', '/requestPasswordReset', () => { throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'This path is not implemented yet.'); }); + this.route('POST', '/requestPasswordReset', req => this.handleReset(req)); } } diff --git a/src/index.js b/src/index.js index fb8d29efed..e20fcbcbc0 100644 --- a/src/index.js +++ b/src/index.js @@ -11,31 +11,31 @@ var batch = require('./batch'), multer = require('multer'), Parse = require('parse/node').Parse, httpRequest = require('./httpRequest'); - -import PromiseRouter from './PromiseRouter'; -import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; -import { S3Adapter } from './Adapters/Files/S3Adapter'; -import { FilesController } from './Controllers/FilesController'; 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 verifyEmail from './verifyEmail'; +import loadAdapter from './Adapters/loadAdapter'; 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 { loadAdapter } from './Adapters/AdapterLoader'; -import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; +import { InstallationsRouter } from './Routers/InstallationsRouter'; +import AdapterLoader from './Adapters/AdapterLoader'; import { LoggerController } from './Controllers/LoggerController'; +import { LogsRouter } from './Routers/LogsRouter'; +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'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -64,8 +64,23 @@ addParseCloud(); // "javascriptKey": optional key from Parse dashboard // "push": optional key from configure push +let validateEmailConfiguration = (verifyUserEmails, appName, emailAdapter) => { + if (verifyUserEmails) { + if (typeof appName !== 'string') { + throw 'An app name is required when using email verification.'; + } + if (!emailAdapter) { + throw 'User email verification was enabled, but no email adapter was provided'; + } + if (typeof emailAdapter.sendVerificationEmail !== 'function') { + throw 'Invalid email adapter: no sendVerificationEmail() function was provided'; + } + } +} + function ParseServer({ appId, + appName, masterKey, databaseAdapter, filesAdapter, @@ -83,6 +98,8 @@ function ParseServer({ enableAnonymousUsers = true, oauth = {}, serverURL = '', + verifyUserEmails = false, + emailAdapter, }) { if (!appId || !masterKey) { throw 'You must provide an appId and masterKey!'; @@ -105,18 +122,13 @@ function ParseServer({ throw "argument 'cloud' must either be a string or a function"; } } - - - const filesControllerAdapter = loadAdapter(filesAdapter, GridStoreAdapter); - const pushControllerAdapter = loadAdapter(push, ParsePushAdapter); - const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); // 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(AdapterLoader.loadAdapter(filesAdapter, GridStoreAdapter)); + const pushController = new PushController(AdapterLoader.loadAdapter(push, ParsePushAdapter)); + const loggerController = new LoggerController(AdapterLoader.loadAdapter(loggerAdapter, FileLoggerAdapter)); + cache.apps[appId] = { masterKey: masterKey, collectionPrefix: collectionPrefix, @@ -131,9 +143,17 @@ function ParseServer({ loggerController: loggerController, enableAnonymousUsers: enableAnonymousUsers, oauth: oauth, -}; + appName: appName, + }; + + if (verifyUserEmails && process.env.PARSE_EXPERIMENTAL_EMAIL_VERIFICATION_ENABLED || process.env.TESTING == 1) { + emailAdapter = loadAdapter(emailAdapter); + validateEmailConfiguration(verifyUserEmails, appName, emailAdapter); + cache.apps[appId].verifyUserEmails = verifyUserEmails; + cache.apps[appId].emailAdapter = emailAdapter; + } - // 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[appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } @@ -148,6 +168,11 @@ function ParseServer({ // File handling needs to be before default middlewares are applied api.use('/', new FilesRouter().getExpressRouter()); + if (process.env.PARSE_EXPERIMENTAL_EMAIL_VERIFICATION_ENABLED || process.env.TESTING == 1) { + //api.use('/request_password_reset', passwordReset.reset(appName, appId)); + //api.get('/password_reset_success', passwordReset.success); + api.get('/verify_email', verifyEmail(appId, serverURL)); + } // TODO: separate this from the regular ParseServer object if (process.env.TESTING == 1) { @@ -172,7 +197,7 @@ function ParseServer({ new LogsRouter(), new IAPValidationRouter() ]; - + if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { routers.push(require('./global_config')); } @@ -244,5 +269,5 @@ function getClassName(parseClass) { module.exports = { ParseServer: ParseServer, - S3Adapter: S3Adapter + S3Adapter: S3Adapter, }; diff --git a/src/transform.js b/src/transform.js index f254f0d464..7ff570c063 100644 --- a/src/transform.js +++ b/src/transform.js @@ -42,6 +42,9 @@ export function transformKeyValue(schema, className, restKey, restValue, options key = '_updated_at'; timeField = true; break; + case '_email_verify_token': + key = "_email_verify_token"; + break; case 'sessionToken': case '_session_token': key = '_session_token'; @@ -649,7 +652,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/src/verifyEmail.js b/src/verifyEmail.js new file mode 100644 index 0000000000..5bd1da3269 --- /dev/null +++ b/src/verifyEmail.js @@ -0,0 +1,27 @@ +function verifyEmail(appId, serverURL) { + var DatabaseAdapter = require('./DatabaseAdapter'); + var database = DatabaseAdapter.getDatabaseConnection(appId); + return (req, res) => { + var token = req.query.token; + var username = req.query.username; + if (!token || !username) { + res.redirect(302, serverURL + '/invalid_link.html'); + return; + } + database.collection('_User').then(coll => { + // Need direct database access because verification token is not a parse field + coll.findAndModify({ + username: username, + _email_verify_token: token, + }, null, {$set: {emailVerified: true}}, (err, doc) => { + if (err || !doc.value) { + res.redirect(302, serverURL + '/invalid_link.html'); + } else { + res.redirect(302, serverURL + '/verify_email_success.html?username=' + username); + } + }); + }); + } +} + +module.exports = verifyEmail;