UNPKG

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