Skip to content

Commit 4a53faa

Browse files
committed
Exploring the interface of a mail adapter
1 parent 8ce8e2b commit 4a53faa

File tree

9 files changed

+287
-37
lines changed

9 files changed

+287
-37
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"body-parser": "^1.14.2",
1818
"deepcopy": "^0.6.1",
1919
"express": "^4.13.4",
20+
"mailgun-js": "^0.7.7",
2021
"mime": "^1.3.4",
2122
"mongodb": "~2.1.0",
2223
"multer": "^1.1.0",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Mailgun from 'mailgun-js';
2+
3+
export default (mailgunOptions) => {
4+
let mailgun = Mailgun(mailgunOptions);
5+
6+
let sendMail = (to, subject, text) => {
7+
let data = {
8+
from: mailgunOptions.fromAddress,
9+
to: to,
10+
subject: subject,
11+
text: text,
12+
}
13+
14+
return new Promise((resolve, reject) => {
15+
mailgun.messages().send(data, (err, body) => {
16+
if (typeof err !== 'undefined') {
17+
reject(err);
18+
}
19+
resolve(body);
20+
});
21+
});
22+
}
23+
24+
return {
25+
sendVerificationEmail: ({ link, user, appName, }) => {
26+
let verifyMessage =
27+
"Hi,\n\n" +
28+
"You are being asked to confirm the e-mail address " + user.email + " with " + appName + "\n\n" +
29+
"" +
30+
"Click here to confirm it:\n" + link;
31+
sendMail(user.email, 'Please verify your e-mail for ' + appName, verifyMessage);
32+
}
33+
}
34+
}

src/Config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ export class Config {
2424
this.facebookAppIds = cacheInfo.facebookAppIds;
2525
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
2626

27+
this.verifyUserEmails = cacheInfo.verifyUserEmails;
28+
this.emailAdapter = cacheInfo.emailAdapter;
29+
2730
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
2831
this.filesController = cacheInfo.filesController;
29-
this.pushController = cacheInfo.pushController;
32+
this.pushController = cacheInfo.pushController;
3033
this.loggerController = cacheInfo.loggerController;
3134
this.oauth = cacheInfo.oauth;
3235

src/RestWrite.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,11 @@ RestWrite.prototype.transformUser = function() {
404404
throw new Parse.Error(Parse.Error.USERNAME_TAKEN,
405405
'Account already exists for this username');
406406
}
407+
if (this.config.verifyUserEmails && this.data.email) {
408+
this.data.emailVerified = false;
409+
this.data._email_verify_token = cryptoUtils.randomString(25);
410+
this.data._perishable_token = cryptoUtils.randomString(25);
411+
}
407412
return Promise.resolve();
408413
});
409414
}).then(() => {
@@ -716,10 +721,23 @@ RestWrite.prototype.runDatabaseOperation = function() {
716721
throw new Parse.Error(Parse.Error.INVALID_ACL, 'Invalid ACL.');
717722
}
718723

724+
function sendEmailVerification() {
725+
var hasUserEmail = typeof this.data.email !== 'undefined' && this.className === "_User";
726+
if (hasUserEmail && this.config.verifyUserEmails) {
727+
let link = this.config.mount + "/verify_email?token=" + encodeURIComponent(this.data._email_verify_token) + "&username=" + encodeURIComponent(this.data.username);
728+
this.config.emailAdapter.sendVerificationEmail({
729+
link: link,
730+
user: this.auth.user,
731+
appName: this.co.appName,
732+
});
733+
}
734+
}
735+
719736
if (this.query) {
720737
// Run an update
721738
return this.config.database.update(
722739
this.className, this.query, this.data, this.runOptions).then((resp) => {
740+
sendEmailVerification.call(this);
723741
this.response = resp;
724742
this.response.updatedAt = this.updatedAt;
725743
});
@@ -733,6 +751,9 @@ RestWrite.prototype.runDatabaseOperation = function() {
733751
}
734752
// Run a create
735753
return this.config.database.create(this.className, this.data, this.runOptions)
754+
.then(() => {
755+
sendEmailVerification.call(this);
756+
})
736757
.then(() => {
737758
var resp = {
738759
objectId: this.data.objectId,

src/Routers/UsersRouter.js

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// These methods handle the User-related routes.
22

3-
import deepcopy from 'deepcopy';
3+
import deepcopy from 'deepcopy';
44

5-
import ClassesRouter from './ClassesRouter';
6-
import PromiseRouter from '../PromiseRouter';
7-
import rest from '../rest';
8-
import Auth from '../Auth';
5+
import ClassesRouter from './ClassesRouter';
6+
import PromiseRouter from '../PromiseRouter';
7+
import rest from '../rest';
8+
import Auth from '../Auth';
99
import passwordCrypto from '../password';
10-
import RestWrite from '../RestWrite';
11-
import { newToken } from '../cryptoUtils';
10+
import RestWrite from '../RestWrite';
11+
import { newToken } from '../cryptoUtils';
1212

1313
export class UsersRouter extends ClassesRouter {
1414
handleFind(req) {
@@ -138,6 +138,36 @@ export class UsersRouter extends ClassesRouter {
138138
return Promise.resolve(success);
139139
}
140140

141+
handleReset(req) {
142+
if (!req.body.email && req.query.email) {
143+
req.body = req.query;
144+
}
145+
146+
if (!req.body.email) {
147+
throw new Parse.Error(Parse.Error.EMAIL_MISSING,
148+
'email is required.');
149+
}
150+
151+
return req.database.find('_User', {email: req.body.email})
152+
.then((results) => {
153+
if (!results.length) {
154+
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND,
155+
'Email not found.');
156+
}
157+
var emailSender = req.info.app && req.info.app.emailSender;
158+
if (!emailSender) {
159+
throw new Error("No email sender function specified");
160+
}
161+
var perishableSessionToken = encodeURIComponent(results[0].perishableSessionToken);
162+
var encodedEmail = encodeURIComponent(req.body.email)
163+
var endpoint = req.config.mount + "/request_password_reset?token=" + perishableSessionToken + "&username=" + encodedEmail;
164+
return emailSender(Constants.RESET_PASSWORD, endpoint,req.body.email);
165+
})
166+
.then(()=>{
167+
return {response:{}};
168+
})
169+
}
170+
141171
mountRoutes() {
142172
this.route('GET', '/users', req => { return this.handleFind(req); });
143173
this.route('POST', '/users', req => { return this.handleCreate(req); });
@@ -147,9 +177,7 @@ export class UsersRouter extends ClassesRouter {
147177
this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
148178
this.route('GET', '/login', req => { return this.handleLogIn(req); });
149179
this.route('POST', '/logout', req => { return this.handleLogOut(req); });
150-
this.route('POST', '/requestPasswordReset', () => {
151-
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'This path is not implemented yet.');
152-
});
180+
this.route('POST', '/requestPasswordReset', req => this.handleReset(req));
153181
}
154182
}
155183

src/index.js

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,31 @@ var batch = require('./batch'),
1111
multer = require('multer'),
1212
Parse = require('parse/node').Parse,
1313
httpRequest = require('./httpRequest');
14-
15-
import PromiseRouter from './PromiseRouter';
16-
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
17-
import { S3Adapter } from './Adapters/Files/S3Adapter';
18-
import { FilesController } from './Controllers/FilesController';
1914

2015
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
21-
import { PushController } from './Controllers/PushController';
22-
23-
import { ClassesRouter } from './Routers/ClassesRouter';
24-
import { InstallationsRouter } from './Routers/InstallationsRouter';
25-
import { UsersRouter } from './Routers/UsersRouter';
26-
import { SessionsRouter } from './Routers/SessionsRouter';
27-
import { RolesRouter } from './Routers/RolesRouter';
16+
import passwordReset from './passwordReset';
17+
import PromiseRouter from './PromiseRouter';
18+
import SimpleMailgunAdapter from './Adapters/Email/SimpleMailgunAdapter';
19+
import verifyEmail from './verifyEmail';
20+
import { AdapterLoader } from './Adapters/AdapterLoader';
2821
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
22+
import { ClassesRouter } from './Routers/ClassesRouter';
23+
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
24+
import { FilesController } from './Controllers/FilesController';
25+
import { FilesRouter } from './Routers/FilesRouter';
2926
import { FunctionsRouter } from './Routers/FunctionsRouter';
30-
import { SchemasRouter } from './Routers/SchemasRouter';
27+
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
3128
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
32-
import { PushRouter } from './Routers/PushRouter';
33-
import { FilesRouter } from './Routers/FilesRouter';
34-
import { LogsRouter } from './Routers/LogsRouter';
35-
36-
import { AdapterLoader } from './Adapters/AdapterLoader';
37-
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
29+
import { InstallationsRouter } from './Routers/InstallationsRouter';
3830
import { LoggerController } from './Controllers/LoggerController';
31+
import { LogsRouter } from './Routers/LogsRouter';
32+
import { PushController } from './Controllers/PushController';
33+
import { PushRouter } from './Routers/PushRouter';
34+
import { RolesRouter } from './Routers/RolesRouter';
35+
import { S3Adapter } from './Adapters/Files/S3Adapter';
36+
import { SchemasRouter } from './Routers/SchemasRouter';
37+
import { SessionsRouter } from './Routers/SessionsRouter';
38+
import { UsersRouter } from './Routers/UsersRouter';
3939

4040
// Mutate the Parse object to add the Cloud Code handlers
4141
addParseCloud();
@@ -66,6 +66,7 @@ addParseCloud();
6666

6767
function ParseServer({
6868
appId,
69+
appName,
6970
masterKey,
7071
databaseAdapter,
7172
filesAdapter,
@@ -83,6 +84,8 @@ function ParseServer({
8384
enableAnonymousUsers = true,
8485
oauth = {},
8586
serverURL = '',
87+
verifyUserEmails = false,
88+
emailAdapter,
8689
}) {
8790
if (!appId || !masterKey) {
8891
throw 'You must provide an appId and masterKey!';
@@ -105,8 +108,7 @@ function ParseServer({
105108
throw "argument 'cloud' must either be a string or a function";
106109
}
107110
}
108-
109-
111+
110112
const filesControllerAdapter = AdapterLoader.load(filesAdapter, GridStoreAdapter);
111113
const pushControllerAdapter = AdapterLoader.load(push, ParsePushAdapter);
112114
const loggerControllerAdapter = AdapterLoader.load(loggerAdapter, FileLoggerAdapter);
@@ -116,7 +118,19 @@ function ParseServer({
116118
const filesController = new FilesController(filesControllerAdapter);
117119
const pushController = new PushController(pushControllerAdapter);
118120
const loggerController = new LoggerController(loggerControllerAdapter);
119-
121+
122+
if (verifyUserEmails) {
123+
if (typeof appName !== 'string') {
124+
throw 'An app name is required when using email verification.';
125+
}
126+
if (!emailAdapter) {
127+
throw 'User email verification was enabled, but no email adapter was provided';
128+
}
129+
if (typeof emailAdapter.sendVerificationEmail !== 'function') {
130+
throw 'Invalid email adapter: no sendVerificationEmail() function was provided';
131+
}
132+
}
133+
120134
cache.apps[appId] = {
121135
masterKey: masterKey,
122136
collectionPrefix: collectionPrefix,
@@ -131,7 +145,8 @@ function ParseServer({
131145
loggerController: loggerController,
132146
enableAnonymousUsers: enableAnonymousUsers,
133147
oauth: oauth,
134-
};
148+
verifyUserEmails: verifyUserEmails,
149+
};
135150

136151
// To maintain compatibility. TODO: Remove in v2.1
137152
if (process.env.FACEBOOK_APP_ID) {
@@ -148,6 +163,9 @@ function ParseServer({
148163

149164
// File handling needs to be before default middlewares are applied
150165
api.use('/', new FilesRouter().getExpressRouter());
166+
api.use('/request_password_reset', passwordReset.reset(appName, appId));
167+
api.get('/password_reset_success', passwordReset.success);
168+
api.get('/verify_email', verifyEmail);
151169

152170
// TODO: separate this from the regular ParseServer object
153171
if (process.env.TESTING == 1) {
@@ -172,7 +190,7 @@ function ParseServer({
172190
new LogsRouter(),
173191
new IAPValidationRouter()
174192
];
175-
193+
176194
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
177195
routers.push(require('./global_config'));
178196
}
@@ -233,5 +251,6 @@ function getClassName(parseClass) {
233251

234252
module.exports = {
235253
ParseServer: ParseServer,
236-
S3Adapter: S3Adapter
254+
S3Adapter: S3Adapter,
255+
SimpleMailgunAdapter: SimpleMailgunAdapter,
237256
};

0 commit comments

Comments
 (0)