diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 14b4a53da1..a7f355d2b9 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -2,6 +2,10 @@ "missingAuthHeaders": "Missing authentication headers.", "missingUsernameEmail": "Missing username or email.", "missingPassword": "Missing password.", + "invalidEmail": "Invalid email address.", + "emailTaken": "Email already taken.", + "usernameTaken": "Username already taken.", + "passwordConfirmationMatch": "Password confirmation doesn't match password.", "invalidLoginCredentials": "Incorrect username / email and / or password.", "invalidCredentials": "User not found with given auth credentials.", "accountSuspended": "Account has been suspended, please contact leslie@habitica.com with your UUID \"<%= userId %>\" for assistance." diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index f85cb9455a..afd8d24fe4 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -11,6 +11,7 @@ gulp.task('lint:server', () => { .src([ './website/src/**/api-v3/**/*.js', './website/src/models/user.js', + './website/src/models/emailUnsubscription.js', './website/src/server.js' ]) .pipe(eslint()) diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index b6b1f1356d..15d7d84740 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -4,7 +4,9 @@ import { } from '../../libs/api-v3/errors'; import passwordUtils from '../../libs/api-v3/password'; import User from '../../models/user'; - +import EmailUnsubscription from '../../models/emailUnsubscription'; +import Q from 'q'; +import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email'; let api = {}; /** @@ -18,14 +20,115 @@ let api = {}; * @apiParam {String} password Password for the new user account * @apiParam {String} passwordConfirmation Password confirmation * - * @apiSuccess {Object} user The user public fields - * + * @apiSuccess {Object} user The user profile * * @apiUse NotAuthorized */ api.registerLocal = { method: 'POST', url: '/user/register/local', + handler (req, res, next) { + req.checkBody({ + username: { + notEmpty: true, + errorMessage: req.t('missingEmail'), + }, + email: { + notEmpty: true, + isEmail: true, + errorMessage: req.t('invalidEmail'), + }, + password: { + notEmpty: true, + errorMessage: req.t('missingPassword'), + }, + passwordConfirmation: { + notEmpty: true, + equals: { + options: [req.body.password], + }, + errorMessage: req.t('passwordConfirmationMatch'), + }, + }); + + let validationErrors = req.validationErrors(); + + if (validationErrors) return next(validationErrors); + + req.sanitizeBody('username').trim(); + req.sanitizeBody('email').trim(); + req.sanitizeBody('password').trim(); + req.sanitizeBody('passwordConfirmation').trim(); + + let email = req.body.email.toLowerCase(); + let username = req.body.username; + // Get the lowercase version of username to check that we do not have duplicates + // So we can search for it in the database and then reject the choosen username if 1 or more results are found + let lowerCaseUsername = username.toLowerCase(); + + Q.all([ + // Search for duplicates using lowercase version of username + User.findOne({$or: [ + {'auth.local.email': email}, + {'auth.local.lowerCaseUsername': lowerCaseUsername}, + ]}, {'auth.local': 1}) + .exec(), + + // If the request is made by an authenticated Facebook user, find it + // TODO move to a separate route + // TODO automatically merge? + /* User.findOne({ + _id: req.headers['x-api-user'], + apiToken: req.headers['x-api-key'] + }, {auth:1}) + .exec(); */ + ]) + .then((results) => { + if (results[0]) { + if (email === results[0].auth.local.email) return next(new NotAuthorized(req.t('emailTaken'))); + // Check that the lowercase username isn't already used + if (lowerCaseUsername === results[0].auth.local.lowerCaseUsername) return next(new NotAuthorized(req.t('usernameTaken'))); + } + + let salt = passwordUtils.makeSalt(); + let newUser = new User({ + auth: { + local: { + username, + lowerCaseUsername, // Store the lowercase version of the username + email, // Store email as lowercase + salt, + hashed_password: passwordUtils.encrypt(req.body.password, salt), // eslint-disable-line camelcase + }, + }, + preferences: { + language: req.language, + }, + }); + + newUser.registeredThrough = req.headers['x-client']; // TODO is this saved somewhere? + + res.analytics.track('register', { + category: 'acquisition', + type: 'local', + gaLabel: 'local', + uuid: newUser._id, + }); + + return newUser.save(); + }) + .then((savedUser) => { + res.status(201).json(savedUser); + + // Clean previous email preferences + EmailUnsubscription + .remove({email: savedUser.auth.local.email}) + .then(() => { + sendTxnEmail(savedUser, 'welcome'); + }); + }) + .catch(next); + }, }; /** diff --git a/website/src/libs/api-v3/setupRoutes.js b/website/src/libs/api-v3/setupRoutes.js index 8c346577bc..bdb587d50c 100644 --- a/website/src/libs/api-v3/setupRoutes.js +++ b/website/src/libs/api-v3/setupRoutes.js @@ -14,7 +14,7 @@ fs let controller = require(CONTROLLERS_PATH + fileName); // eslint-disable-line global-require _.each(controller, (action) => { - let {method, url, middlewares, handler} = action; + let {method, url, middlewares = [], handler} = action; method = method.toLowerCase(); router[method](url, ...middlewares, handler); diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js index 8e0be1e669..0f9838c1e6 100644 --- a/website/src/middlewares/api-v3/getUserLanguage.js +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -57,6 +57,7 @@ function _getFromUser (user, req) { } function _attachTranslateFunction (req, next) { + // TODO attach to res? req.t = function reqTranslation () { return i18n.t(...arguments, req.language); }; diff --git a/website/src/models/emailUnsubscription.js b/website/src/models/emailUnsubscription.js index 144417f3fc..4d0594c0c5 100644 --- a/website/src/models/emailUnsubscription.js +++ b/website/src/models/emailUnsubscription.js @@ -1,14 +1,20 @@ -var mongoose = require("mongoose"); -var shared = require('../../../common'); +import mongoose from 'mongoose'; +import common from '../../../common'; +import validator from 'validator'; // A collection used to store mailing list unsubscription for non registered email addresses -var EmailUnsubscriptionSchema = new mongoose.Schema({ +export let schema = new mongoose.Schema({ _id: { type: String, - 'default': shared.uuid + default: common.uuid, + }, + email: { + type: String, + required: true, + trim: true, + lowercase: true, // TODO migrate existing to lowerCase + validator: [validator.isEmail, 'Invalid email.'], }, - email: String }); -module.exports.schema = EmailUnsubscriptionSchema; -module.exports.model = mongoose.model('EmailUnsubscription', EmailUnsubscriptionSchema); \ No newline at end of file +export let model = mongoose.model('EmailUnsubscription', schema);