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;