UNPKG

11.1 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.checkEmailExists = checkEmailExists;
7exports.generateUserId = generateUserId;
8exports.generateActivationHash = generateActivationHash;
9exports.generateHash = generateHash;
10exports.getDefaultIsAdmin = getDefaultIsAdmin;
11exports.getDefaultUserSlug = getDefaultUserSlug;
12exports.useSignup = useSignup;
13
14var _bcryptNodejs = require('bcrypt-nodejs');
15
16var _bcryptNodejs2 = _interopRequireDefault(_bcryptNodejs);
17
18var _lodash = require('lodash.kebabcase');
19
20var _lodash2 = _interopRequireDefault(_lodash);
21
22var _mongodb = require('mongodb');
23
24var _passport = require('passport');
25
26var _passport2 = _interopRequireDefault(_passport);
27
28var _passportLocal = require('passport-local');
29
30var _shortid = require('shortid');
31
32var _shortid2 = _interopRequireDefault(_shortid);
33
34var _format = require('./format');
35
36var _jwt = require('./jwt');
37
38function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
39
40// from: http://stackoverflow.com/a/46181
41// NOTE: is it good enough?
42/* eslint-disable */
43var VALID_EMAIL_REGEXP = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
44var MIN_PASSWORD_LENGTH = 6;
45var globalUserFields = [];
46
47function checkEmailExists(email, usersCollection) {
48 return new Promise(function (resolve) {
49 usersCollection.findOne({ 'local.email': email }).then(function (existingUser) {
50 resolve(!!existingUser);
51 });
52 });
53}
54
55function generateUserId() {
56 return 'user_' + _shortid2.default.generate();
57}
58
59// hack to create a longer id here (without additional dependency),
60// to decrease likelyhood that someone can guess/bruteforce the id
61function generateActivationHash() {
62 return _shortid2.default.generate() + _shortid2.default.generate();
63}
64
65function generateHash(password) {
66 return new Promise(function (resolve, reject) {
67 _bcryptNodejs2.default.genSalt(8, function (err, result) {
68 if (err) {
69 reject(err);
70 return;
71 }
72 return resolve(result);
73 });
74 }).then(function (salt) {
75 return new Promise(function (resolve, reject) {
76 _bcryptNodejs2.default.hash(password, salt, null, function (err, hashed) {
77 if (err) {
78 reject(err);
79 return;
80 }
81 return resolve(hashed);
82 });
83 });
84 });
85}
86
87function getDefaultIsAdmin(user) {
88 return false;
89}
90
91function getDefaultUserSlug(user) {
92 var name = user.firstName + '-' + user.lastName;
93 return (0, _lodash2.default)(name.toString());
94}
95
96function useSignup(app, config) {
97 // unpack
98 var createJWTforUser = config.createJWTforUser,
99 db = config.db,
100 logger = config.logger,
101 _config$mailer = config.mailer,
102 accountName = _config$mailer.accountName,
103 activationRoute = _config$mailer.activationRoute,
104 senderName = _config$mailer.senderName,
105 projectName = _config$mailer.projectName,
106 send = _config$mailer.send,
107 senderMail = _config$mailer.senderMail,
108 requiredFields = config.requiredFields,
109 routePath = config.routePath;
110
111 var getFormatUser = config.getFormatUser || _format.getFormatUser;
112 var getIsAdmin = config.getIsAdmin || getDefaultIsAdmin;
113 var getUserSlug = config.getUserSlug || getDefaultUserSlug;
114 var endpoint = config.endpoint || 'signup';
115 var bodyKey = endpoint + 'Body';
116 var messageKey = endpoint + 'Message';
117 var passportKey = 'local-' + endpoint;
118 // users
119 var usersCollection = db.collection('users');
120 // passport hook
121 function signupUser(signupData, usersCollection) {
122 return new Promise(function (resolve, reject) {
123 // any existing user with this email?
124 checkEmailExists(signupData.email, usersCollection).then(function (emailExists) {
125 if (emailExists) {
126 logger.debug('signupUser: email already existed');
127 reject(new Error('USER_EMAIL_ALREADY_TAKEN'));
128 } else {
129 logger.debug('signupUser: this email is new');
130 }
131 // validate the new user
132 return new Promise(function (resolve) {
133 var validationError = validateUserSignup(signupData);
134 if (validationError) {
135 reject(validationError);
136 return;
137 }
138 resolve(signupData);
139 });
140 }).then(function (validated) {
141 if (!validated) return;
142 // new signup
143 var newUser = {
144 id: generateUserId(),
145 // id: ObjectId(),
146 created_at: Date.now(),
147 activationHash: generateActivationHash(),
148 active: false,
149 local: Object.keys(signupData)
150 // TODO: in future, probably invert logic - explicitly list what should go into local,
151 // and the rest goes globally at root level on new user object
152 .reduce(function (newUserLocal, key) {
153 // for now just drop empty fields (''),
154 // and fields that go globally on user
155 if (signupData[key].length && !globalUserFields.includes(key)) {
156 newUserLocal[key] = signupData[key];
157 }
158 return newUserLocal;
159 }, {}),
160 // it is a slug helpful for retrieving the user via an url
161 slug: getUserSlug(signupData)
162 };
163 return newUser;
164 }).then(function (validatedNewUser) {
165 if (!validatedNewUser) return;
166 // hash the password
167 // NOTE: we do this after validation, so we catch short passwords first
168 return generateHash(signupData.password).then(function (hashedPassword) {
169 validatedNewUser.local.password = hashedPassword;
170 return validatedNewUser;
171 });
172 }).then(function (newUserWithHashedPassword) {
173 if (!newUserWithHashedPassword) return;
174 // save user and return
175 return usersCollection.insertOne(newUserWithHashedPassword).then(function () {
176 // DONE
177 resolve(newUserWithHashedPassword);
178 });
179 });
180 });
181 }
182 // NOTE: only returning first error, not all..
183 // all modern browsers except Safari should catch the errors in browser.
184 function validateUserSignup(user) {
185 if (!VALID_EMAIL_REGEXP.test(user.email)) {
186 return new Error('Please enter a valid email');
187 }
188 if (user.password.length < MIN_PASSWORD_LENGTH) {
189 return new Error('Password must be at least 6 characters long');
190 }
191 var error = void 0;
192 requiredFields.forEach(function (field) {
193 if (!user[field] || user[field].trim().length === 0) {
194 error = new Error(field + ' is required');
195 return;
196 }
197 });
198 return error;
199 }
200 function sendActivationEmail(email, activationHash) {
201 var activationUrl = activationRoute + '=' + activationHash;
202 var mailerConfig = {
203 from: projectName + ' <' + senderMail + '>',
204 to: email,
205 subject: 'Confirm your ' + accountName + ' account',
206 html: '<h1>Account confirmation</h1>\n <p>Thanks for signing up.</p>\n <p>Please click on the following link to activate your ' + accountName + ' account:</p>\n <p><a href="' + activationUrl + '">' + activationUrl + '</a></p>\n <br />\n <p>If you did not create this account, please ignore it and the created account will be deleted.<p>\n <br />\n <p>If clicking the link above does not work, please copy and paste the URL into a new browser window instead.<p>\n <br />\n <p>If you have any questions or problems, please let us know by repling to this email.<p>\n <p>Thanks,\n ' + senderName + ' from ' + projectName + '</p>\n '
207 };
208 send(mailerConfig).catch(function (err) {
209 logger.warn('[unhandled] ERROR in sendActivationEmail:', err);
210 });
211 }
212
213 // (local) SIGNUP
214 // serialize
215 // used for serializing the user for the session
216 _passport2.default.serializeUser(function (user, done) {
217 done(null, user.id);
218 });
219 // de-serializing
220 _passport2.default.deserializeUser(function (id, done) {
221 usersCollection.findOne({ id: id }).then(function (user) {
222 if (!user) {
223 done(null, false);
224 return;
225 }
226 var formatUser = getFormatUser(user);
227 // add auth token, for api access
228 if (!user.authToken || !user.authTokenExpiry) {
229 var jwtConfig = createJWTforUser(formatUser);
230 formatUser.authToken = jwtConfig.token;
231 formatUser.authTokenExpiry = jwtConfig.expiry;
232 }
233 done(null, formatUser);
234 }).catch(done);
235 });
236 _passport2.default.use(passportKey, new _passportLocal.Strategy({
237 usernameField: 'email',
238 passwordField: 'password',
239 passReqToCallback: true // get access to full request in callback
240 }, function (req, email, password, done) {
241 // store the pushed data so we can keep the form populated in case of failure
242 req.flash(bodyKey, req.body);
243 signupUser(req.body, usersCollection).then(function (finalUser) {
244 // NOTE: sending this email is async; not blocking on this.
245 sendActivationEmail(email, finalUser.activationHash);
246 // done
247 done(null, finalUser);
248 return;
249 }).catch(function (err) {
250 if (err.message === 'USER_EMAIL_ALREADY_TAKEN') {
251 done(null, false, req.flash(messageKey, 'That email is already taken.'));
252 }
253 // TODO: handle this error!
254 logger.warn('[unhandled] ERROR in handleSignup:', err);
255 // done(null, false, req.flash('signup', validationError.message))
256 done(null, false, req.flash(messageKey, err.message || 'Signup failed.'));
257 });
258 }));
259 // apis
260 app.post(routePath + '/' + endpoint, _passport2.default.authenticate(passportKey, {
261 successRedirect: '/',
262 failureRedirect: '/' + endpoint,
263 failureFlash: true // allow flash messages
264 }));
265 // if some server problem causes user to try to
266 // reach these endpoints via GET requests from browser
267 app.get(routePath + '/' + endpoint, function (req, res) {
268 return res.redirect('/');
269 });
270 // activate
271 app.post(routePath + '/activate-account', function (req, res) {
272 var code = req.body.code;
273
274 if (!code) {
275 res.json({ error: 'Activation code required.' });
276 return;
277 }
278 usersCollection.findOneAndUpdate({ activationHash: code }, { $set: {
279 active: true,
280 admin: getIsAdmin(req.user)
281 } },
282 // return updated
283 { returnOriginal: false }).then(function (_ref) {
284 var user = _ref.value;
285
286 if (!user) {
287 res.json({ error: 'Invalid activation code' });
288 return;
289 }
290 res.json({ user: getFormatUser(user) });
291 }).catch(function (err) {
292 res.json({ error: err.message });
293 return;
294 });
295 });
296 // reactivate
297 app.get(routePath + '/ask-activate-account', function (req, res) {
298 usersCollection.findOne({
299 id: req.user.id
300 }).then(function (_ref2) {
301 var activationHash = _ref2.activationHash,
302 email = _ref2.local.email;
303
304 sendActivationEmail(email, activationHash);
305 res.json({
306 text: 'We sent you an email to ' + email
307 });
308 });
309 });
310}
\No newline at end of file