Refactors verify_email, adds public html

This commit is contained in:
Florent Vilmart
2016-02-25 19:04:27 -05:00
parent 0b307bc22f
commit 7dd765256c
14 changed files with 455 additions and 63 deletions

View File

@@ -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;

View File

@@ -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()");
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -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);

View File

@@ -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;