From 7dd765256c0d479d9a0ca9dda726726d9a1572b1 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 25 Feb 2016 19:04:27 -0500 Subject: [PATCH] Refactors verify_email, adds public html --- public_html/choose_password.html | 175 ++++++++++++++++++++++++ public_html/invalid_link.html | 43 ++++++ public_html/password_reset_success.html | 27 ++++ public_html/verify_email_success.html | 27 ++++ spec/ParseUser.spec.js | 14 +- src/Config.js | 25 +++- src/Controllers/AdaptableController.js | 8 +- src/Controllers/MailController.js | 3 +- src/Controllers/UserController.js | 32 +++++ src/PromiseRouter.js | 37 ++++- src/Routers/PublicAPIRouter.js | 48 +++++++ src/Routers/UsersRouter.js | 8 +- src/index.js | 44 +++--- src/verifyEmail.js | 27 ---- 14 files changed, 455 insertions(+), 63 deletions(-) create mode 100644 public_html/choose_password.html create mode 100644 public_html/invalid_link.html create mode 100644 public_html/password_reset_success.html create mode 100644 public_html/verify_email_success.html create mode 100644 src/Controllers/UserController.js create mode 100644 src/Routers/PublicAPIRouter.js delete mode 100644 src/verifyEmail.js diff --git a/public_html/choose_password.html b/public_html/choose_password.html new file mode 100644 index 00000000..b487862a --- /dev/null +++ b/public_html/choose_password.html @@ -0,0 +1,175 @@ + + + + + Password Reset + + + +

Reset Your Password

+ +
+
+ + + + + + +
+ + + diff --git a/public_html/invalid_link.html b/public_html/invalid_link.html new file mode 100644 index 00000000..66bdc788 --- /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 00000000..774cbb35 --- /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 00000000..774ea38a --- /dev/null +++ b/public_html/verify_email_success.html @@ -0,0 +1,27 @@ + + + + + Email Verification + + +

Successfully verified your email!

+ + diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 64477074..475622cf 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -171,7 +171,7 @@ describe('Parse.User testing', () => { 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'); + 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); @@ -202,21 +202,21 @@ describe('Parse.User testing', () => { }); it('redirects you to invalid link if you try to verify email incorrecly', done => { - request.get('http://localhost:8378/1/verify_email', { + 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/invalid_link.html'); + 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 => { - request.get('http://localhost:8378/1/verify_email?token=asdfasdf&username=sadfasga', { + 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/invalid_link.html'); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); done(); }); }); @@ -225,11 +225,11 @@ describe('Parse.User testing', () => { var user = new Parse.User(); var emailAdapter = { sendVerificationEmail: options => { - request.get('http://localhost:8378/1/verify_email?token=invalid&username=zxcv', { + 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/invalid_link.html'); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); user.fetch() .then(() => { expect(user.get('emailVerified')).toEqual(false); diff --git a/src/Config.js b/src/Config.js index 1203b0a3..c31f62eb 100644 --- a/src/Config.js +++ b/src/Config.js @@ -25,6 +25,8 @@ export class Config { this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix); this.mailController = cacheInfo.mailController; + + this.serverURL = cacheInfo.serverURL; this.verifyUserEmails = cacheInfo.verifyUserEmails; this.appName = cacheInfo.appName; @@ -32,11 +34,32 @@ export class Config { this.filesController = cacheInfo.filesController; this.pushController = cacheInfo.pushController; this.loggerController = cacheInfo.loggerController; + this.mailController = cacheInfo.mailController; this.oauth = cacheInfo.oauth; this.mount = mount; } -} + + get invalidLinkURL() { + return `${this.serverURL}/apps/invalid_link.html`; + } + + get verifyEmailSuccessURL() { + return `${this.serverURL}/apps/verify_email_success.html`; + } + + get choosePasswordURL() { + return `${this.serverURL}/apps/choose_password`; + } + + get passwordResetSuccessURL() { + return `${this.serverURL}/apps/password_reset_success.html`; + } + + get verifyEmailURL() { + return `${this.serverURL}/apps/${this.applicationId}/verify_email`; + } +}; export default Config; module.exports = Config; diff --git a/src/Controllers/AdaptableController.js b/src/Controllers/AdaptableController.js index 83f3f0a0..cfb0b9af 100644 --- a/src/Controllers/AdaptableController.js +++ b/src/Controllers/AdaptableController.js @@ -10,11 +10,13 @@ based on the parameters passed // _adapter is private, use Symbol var _adapter = Symbol(); +import cache from '../cache'; export class AdaptableController { - constructor(adapter) { + constructor(adapter, appId) { this.adapter = adapter; + this.appId = appId; } set adapter(adapter) { @@ -26,6 +28,10 @@ export class AdaptableController { return this[_adapter]; } + get config() { + return cache.apps[this.appId]; + } + expectedAdapterType() { throw new Error("Subclasses should implement expectedAdapterType()"); } diff --git a/src/Controllers/MailController.js b/src/Controllers/MailController.js index ee467fe6..47d008cc 100644 --- a/src/Controllers/MailController.js +++ b/src/Controllers/MailController.js @@ -13,7 +13,8 @@ export class MailController extends AdaptableController { sendVerificationEmail(user, config) { const token = encodeURIComponent(user._email_verify_token); const username = encodeURIComponent(user.username); - let link = `${config.mount}/verify_email?token=${token}&username=${username}`; + + let link = `${config.verifyEmailURL}?token=${token}&username=${username}`; this.adapter.sendVerificationEmail({ appName: config.appName, link: link, diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js new file mode 100644 index 00000000..62d6dd39 --- /dev/null +++ b/src/Controllers/UserController.js @@ -0,0 +1,32 @@ + +var DatabaseAdapter = require('../DatabaseAdapter'); + +export class UserController { + + constructor(appId) { + this.appId = appId; + } + + verifyEmail(username, token) { + var database = DatabaseAdapter.getDatabaseConnection(this.appId); + return new Promise((resolve, reject) => { + 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(); + } else { + resolve(); + } + }); + }); + + }); + + } +} + +export default UserController; diff --git a/src/PromiseRouter.js b/src/PromiseRouter.js index 8155c796..c3ca10ec 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,19 @@ 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) { + 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)); } + var status = result.status || 200; res.status(status); + if (result.location && !result.response) { + return res.redirect(result.location); + } if (result.location) { res.set('Location', result.location); } diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js new file mode 100644 index 00000000..2b75d3f5 --- /dev/null +++ b/src/Routers/PublicAPIRouter.js @@ -0,0 +1,48 @@ +import PromiseRouter from '../PromiseRouter'; +import UserController from '../Controllers/UserController'; +import Config from '../Config'; +import express from 'express'; +import path from 'path'; + +export class PublicAPIRouter extends PromiseRouter { + + verifyEmail(req) { + var token = req.query.token; + var username = req.query.username; + var appId = req.params.appId; + var config = new Config(appId); + + if (!token || !username) { + return Promise.resolve({ + status: 302, + location: config.invalidLinkURL + }); + } + + let userController = new UserController(appId); + return userController.verifyEmail(username, token, appId).then( () => { + return Promise.resolve({ + status: 302, + location: `${config.verifyEmailSuccessURL}?username=${username}` + }); + }, ()=> { + return Promise.resolve({ + status: 302, + location: config.invalidLinkURL + }); + }) + } + + mountRoutes() { + this.route('GET','/apps/:appId/verify_email', req => { return this.verifyEmail(req); }); + } + + expressApp() { + var router = express(); + router.use("/apps", express.static(path.resolve(__dirname, "../../public"))); + router.use(super.expressApp()); + return router; + } +} + +export default PublicAPIRouter; diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 1e329734..70a76bf5 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -154,6 +154,11 @@ export class UsersRouter extends ClassesRouter { } return Promise.resolve(success); } + + handleReset(req) { + let userController = req.config.userController; + return userController.requestPasswordReset(); + } mountRoutes() { this.route('GET', '/users', req => { return this.handleFind(req); }); @@ -164,9 +169,6 @@ 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 => this.handleReset(req)); } } diff --git a/src/index.js b/src/index.js index 74a63bf9..219ec156 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,6 @@ import cache from './cache'; import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; //import passwordReset from './passwordReset'; import PromiseRouter from './PromiseRouter'; -import verifyEmail from './verifyEmail'; import { AnalyticsRouter } from './Routers/AnalyticsRouter'; import { ClassesRouter } from './Routers/ClassesRouter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; @@ -27,8 +26,10 @@ import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; import { IAPValidationRouter } from './Routers/IAPValidationRouter'; import { LogsRouter } from './Routers/LogsRouter'; import { HooksRouter } from './Routers/HooksRouter'; +import { PublicAPIRouter } from './Routers/PublicAPIRouter'; import { HooksController } from './Controllers/HooksController'; +import { UserController } from './Controllers/UserController'; import { InstallationsRouter } from './Routers/InstallationsRouter'; import { loadAdapter } from './Adapters/AdapterLoader'; import { LoggerController } from './Controllers/LoggerController'; @@ -134,16 +135,23 @@ 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(appId); + let mailController; + + if (verifyUserEmails) { + mailController = new MailController(loadAdapter(emailAdapter)); + } cache.apps.set(appId, { masterKey: masterKey, + serverURL: serverURL, collectionPrefix: collectionPrefix, clientKey: clientKey, javascriptKey: javascriptKey, @@ -155,18 +163,14 @@ function ParseServer({ pushController: pushController, loggerController: loggerController, hooksController: hooksController, + mailController: mailController, + verifyUserEmails: verifyUserEmails, enableAnonymousUsers: enableAnonymousUsers, allowClientClassCreation: allowClientClassCreation, oauth: oauth, appName: appName, }); - if (verifyUserEmails && (process.env.PARSE_EXPERIMENTAL_EMAIL_VERIFICATION_ENABLED || process.env.TESTING == 1)) { - let mailController = new MailController(loadAdapter(emailAdapter)); - cache.apps[appId].mailController = mailController; - cache.apps[appId].verifyUserEmails = verifyUserEmails; - } - // 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); @@ -175,18 +179,17 @@ function ParseServer({ // 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 })); 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)); + api.use('/', new PublicAPIRouter().expressApp()); } + // TODO: separate this from the regular ParseServer object if (process.env.TESTING == 1) { api.use('/', require('./testing-routes').router); @@ -218,13 +221,16 @@ function ParseServer({ 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); + api.use(appRouter.expressApp()); appRouter.mountOnto(api); api.use(middlewares.handleParseErrors); diff --git a/src/verifyEmail.js b/src/verifyEmail.js deleted file mode 100644 index 5bd1da32..00000000 --- a/src/verifyEmail.js +++ /dev/null @@ -1,27 +0,0 @@ -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;