From 8dc37b9d304b459a015aa5ab1f642c5e93e0d3e4 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 16 Feb 2016 23:43:09 -0800 Subject: [PATCH] Exploring the interface of a mail adapter Add some tests and demonstrate the adapter loading interface --- package.json | 1 + spec/MockEmailAdapter.js | 3 + spec/MockEmailAdapterWithOptions.js | 8 + spec/ParseUser.spec.js | 213 ++++++++++++++++++++- spec/index.spec.js | 111 +++++++++++ src/Adapters/AdapterLoader.js | 7 +- src/Adapters/Email/SimpleMailgunAdapter.js | 39 ++++ src/Adapters/loadAdapter.js | 25 +++ src/Config.js | 7 +- src/Routers/UsersRouter.js | 39 +++- src/index.js | 77 +++++--- src/transform.js | 5 +- src/verifyEmail.js | 27 +++ 13 files changed, 525 insertions(+), 37 deletions(-) create mode 100644 spec/MockEmailAdapter.js create mode 100644 spec/MockEmailAdapterWithOptions.js create mode 100644 src/Adapters/Email/SimpleMailgunAdapter.js create mode 100644 src/Adapters/loadAdapter.js create mode 100644 src/verifyEmail.js diff --git a/package.json b/package.json index 9837376d..560e8e99 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/spec/MockEmailAdapter.js b/spec/MockEmailAdapter.js new file mode 100644 index 00000000..e06e27cb --- /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 00000000..fe402e06 --- /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 a36b3cdc..23d41fdd 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -49,6 +49,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) { @@ -1704,7 +1915,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 8b558089..005b9c76 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,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 cfe51ffd..1557324b 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 00000000..2d51173d --- /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 00000000..2ab7b350 --- /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 988efb1e..2391a831 100644 --- a/src/Config.js +++ b/src/Config.js @@ -23,9 +23,14 @@ export class Config { this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers; this.allowClientClassCreation = cacheInfo.allowClientClassCreation; this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix); + + this.verifyUserEmails = cacheInfo.verifyUserEmails; + this.emailAdapter = cacheInfo.emailAdapter; + 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.oauth = cacheInfo.oauth; diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 4cba3edb..79dee41c 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) { @@ -87,7 +107,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; @@ -153,6 +173,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 5062b6b0..247a9274 100644 --- a/src/index.js +++ b/src/index.js @@ -11,32 +11,34 @@ 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 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 { FunctionsRouter } from './Routers/FunctionsRouter'; -import { SchemasRouter } from './Routers/SchemasRouter'; -import { IAPValidationRouter } from './Routers/IAPValidationRouter'; -import { PushRouter } from './Routers/PushRouter'; +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 { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; +import { IAPValidationRouter } from './Routers/IAPValidationRouter'; import { LogsRouter } from './Routers/LogsRouter'; import { HooksRouter } from './Routers/HooksRouter'; -import { loadAdapter } from './Adapters/AdapterLoader'; -import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; -import { LoggerController } from './Controllers/LoggerController'; import { HooksController } from './Controllers/HooksController'; +import { InstallationsRouter } from './Routers/InstallationsRouter'; +import { AdapterLoader } from './Adapters/AdapterLoader'; +import { LoggerController } from './Controllers/LoggerController'; +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'; @@ -69,9 +71,24 @@ 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 = requiredParameter('You must provide an appId!'), masterKey = requiredParameter('You must provide a masterKey!'), + appName, databaseAdapter, filesAdapter, push, @@ -89,7 +106,9 @@ function ParseServer({ allowClientClassCreation = true, oauth = {}, serverURL = requiredParameter('You must provide a serverURL!'), - maxUploadSize = '20mb' + maxUploadSize = '20mb', + verifyUserEmails = false, + emailAdapter, }) { // Initialize the node client SDK automatically @@ -141,10 +160,18 @@ function ParseServer({ hooksController: hooksController, enableAnonymousUsers: enableAnonymousUsers, allowClientClassCreation: allowClientClassCreation, - oauth: oauth + oauth: oauth, + appName: appName, }); - // To maintain compatibility. TODO: Remove in v2.1 + 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 some version that breaks backwards compatability if (process.env.FACEBOOK_APP_ID) { cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } @@ -158,6 +185,12 @@ function ParseServer({ maxUploadSize: maxUploadSize })); + 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) { api.use('/', require('./testing-routes').router); @@ -222,5 +255,5 @@ function addParseCloud() { module.exports = { ParseServer: ParseServer, - S3Adapter: S3Adapter + S3Adapter: S3Adapter, }; diff --git a/src/transform.js b/src/transform.js index f254f0d4..7ff570c0 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 00000000..5bd1da32 --- /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;