diff --git a/README.md b/README.md index a567681..327ddb4 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,11 @@ Uploading a track to the local server requires multiple steps, as uploading is n - Import the [Postman](https://www.postman.com) script "add-track.json" from the "postman-examples" into Postman - In each of the three requests add your user id in the "Pre-request script" tab as the value for the "UserId" variable - As tracks have to be split into smaller parts to get a working upload from the sensor you have to run the three requests in the order of: begin -> add -> end -- View your freshly uploaded track at (http://localhost:4200) -> Home -> Your feed \ No newline at end of file +- View your freshly uploaded track at (http://localhost:4200) -> Home -> Your feed + +### Sending E-Mails +By default in development mode mails are not sent, but instead the mail data is logged to the console. This can be overriden with the `--devSendMails` flag if you start the application like so: `npm run dev -- --devSendMails`. + +Mails are also always sent in production mode! + +For actually sending e-mails the user and password for the SMTP server need to be specified as environment variables. The username is read from `MAILUSER`, and the password is read from `MAILPW`, so in local development startup would like something like this (at least in Linux): `MAILUSER=myuser MAILPW=supersecurepassword npm run dev -- --devSendMails`. \ No newline at end of file diff --git a/_helpers/send-email.js b/_helpers/send-email.js new file mode 100644 index 0000000..b2167b1 --- /dev/null +++ b/_helpers/send-email.js @@ -0,0 +1,15 @@ +const nodemailer = require('nodemailer'); +const config = require('../config/email'); + +module.exports = sendEmail; + +async function sendEmail({ to, subject, html, from = config.emailFrom }) { + if (config.sendMails) { + const transporter = nodemailer.createTransport(config.smtpOptions); + await transporter.sendMail({ from, to, subject, html }); + } else { + console.log({ + to, subject, html, from + }); + } +} \ No newline at end of file diff --git a/_middleware/error-handler.js b/_middleware/error-handler.js new file mode 100644 index 0000000..c7d4cee --- /dev/null +++ b/_middleware/error-handler.js @@ -0,0 +1,19 @@ +module.exports = errorHandler; + +function errorHandler(err, req, res, next) { + switch (true) { + case typeof err === 'string': + // custom application error + const is404 = err.toLowerCase().endsWith('not found'); + const statusCode = is404 ? 404 : 400; + return res.status(statusCode).json({ message: err }); + case err.name === 'ValidationError': + // mongoose validation error + return res.status(400).json({ message: err.message }); + case err.name === 'UnauthorizedError': + // jwt authentication error + return res.status(401).json({ message: 'Unauthorized' }); + default: + return res.status(500).json({ message: err.message }); + } +} \ No newline at end of file diff --git a/_middleware/validate-request.js b/_middleware/validate-request.js new file mode 100644 index 0000000..caa45f4 --- /dev/null +++ b/_middleware/validate-request.js @@ -0,0 +1,19 @@ +module.exports = validateRequest; + +function validateRequest(req, next, schema) { + console.log('validateRequest'); + + const options = { + abortEarly: false, // include all errors + allowUnknown: true, // ignore unknown props + stripUnknown: true // remove unknown props + }; + const { error, value } = schema.validate(req.body, options); + if (error) { + console.log('error: ', error) + next(`Validation error: ${error.details.map(x => x.message).join(', ')}`); + } else { + req.body = value; + next(); + } +} \ No newline at end of file diff --git a/accounts/account.service.js b/accounts/account.service.js new file mode 100644 index 0000000..70936b2 --- /dev/null +++ b/accounts/account.service.js @@ -0,0 +1,149 @@ +const crypto = require("crypto"); +const mongoose = require('mongoose'); +const sendEmail = require('../_helpers/send-email'); +var User = mongoose.model('User'); + +module.exports = { + register, + verifyEmail, + forgotPassword, + validateResetToken, + resetPassword, +}; + +async function register(params, origin) { + const user = await User.findOne({ email: params.email }); + + if (user) { + // send already registered error in email to prevent account enumeration + return await sendAlreadyRegisteredEmail(params.email, origin); + } + + const newUser = new User(); + + newUser.username = params.username; + newUser.email = params.email; + newUser.setPassword(params.password); + newUser.verificationToken = randomTokenString(); + newUser.needsEmailValidation = true; + + await newUser.save(); + + // send email + await sendVerificationEmail(newUser, origin); +} + +async function verifyEmail({ token }) { + const account = await User.findOne({ verificationToken: token }); + + if (!account) throw 'Verification failed'; + + account.needsEmailValidation = false; + account.verificationToken = undefined; + await account.save(); +} + +async function forgotPassword({ email }, origin) { + const account = await User.findOne({ email }); + + console.log('forgotPassword', account, email); + + // always return ok response to prevent email enumeration + if (!account) return; + + // create reset token that expires after 24 hours + account.resetToken = { + token: randomTokenString(), + expires: new Date(Date.now() + 24 * 60 * 60 * 1000) + }; + await account.save(); + + console.log('forgotPassword account saved', account); + + + // send email + await sendPasswordResetEmail(account, origin); +} + +async function validateResetToken({ token }) { + const account = await User.findOne({ + 'resetToken.token': token, + 'resetToken.expires': { $gt: Date.now() } + }); + + if (!account) throw 'Invalid token'; +} + +async function resetPassword({ token, password }) { + const account = await User.findOne({ + 'resetToken.token': token, + 'resetToken.expires': { $gt: Date.now() } + }); + + if (!account) throw 'Invalid token'; + + // update password and remove reset token + account.setPassword(password) + account.resetToken = undefined; + await account.save(); +} + +function randomTokenString() { + return crypto.randomBytes(40).toString('hex'); +} + +async function sendVerificationEmail(account, origin) { + let message; + if (origin) { + const verifyUrl = `${origin}/account/verify-email?token=${account.verificationToken}`; + message = `

Please click the below link to verify your email address:

+

${verifyUrl}

`; + } else { + message = `

Please use the below token to verify your email address with the /account/verify-email api route:

+

${account.verificationToken}

`; + } + + await sendEmail({ + to: account.email, + subject: 'Sign-up Verification API - Verify Email', + html: `

Verify Email

+

Thanks for registering!

+ ${message}` + }); +} + +async function sendAlreadyRegisteredEmail(email, origin) { + let message; + if (origin) { + message = `

If you don't know your password please visit the forgot password page.

`; + } else { + message = `

If you don't know your password you can reset it via the /account/forgot-password api route.

`; + } + + await sendEmail({ + to: email, + subject: 'Sign-up Verification API - Email Already Registered', + html: `

Email Already Registered

+

Your email ${email} is already registered.

+ ${message}` + }); +} + +async function sendPasswordResetEmail(account, origin) { + let message; + if (origin) { + const resetUrl = `${origin}/account/reset-password?token=${account.resetToken.token}`; + message = `

Please click the below link to reset your password, the link will be valid for 1 day:

+

${resetUrl}

`; + } else { + message = `

Please use the below token to reset your password with the /account/reset-password api route:

+

${account.resetToken.token}

`; + } + + await sendEmail({ + to: account.email, + subject: 'Sign-up Verification API - Reset Password', + html: `

Reset Password Email

+ ${message}` + }); +} \ No newline at end of file diff --git a/accounts/accounts.controller.js b/accounts/accounts.controller.js new file mode 100644 index 0000000..680585b --- /dev/null +++ b/accounts/accounts.controller.js @@ -0,0 +1,88 @@ +const express = require('express'); +const router = express.Router(); +const Joi = require('joi'); +const validateRequest = require('../_middleware/validate-request'); +const accountService = require('./account.service'); + +// routes +router.post('/register', registerSchema, register); +router.post('/verify-email', verifyEmailSchema, verifyEmail); +router.post('/forgot-password', forgotPasswordSchema, forgotPassword); +router.post('/validate-reset-token', validateResetTokenSchema, validateResetToken); +router.post('/reset-password', resetPasswordSchema, resetPassword); + +module.exports = router; + +function registerSchema(req, res, next) { + const schema = Joi.object({ + username: Joi.string().required(), + email: Joi.string().email().required(), + password: Joi.string().min(6).required(), + confirmPassword: Joi.string().valid(Joi.ref('password')).required() + }); + + validateRequest(req, next, schema); +} + +function register(req, res, next) { + accountService.register(req.body, req.get('origin')) + .then(() => res.json({ message: 'Registration successful, please check your email for verification instructions' })) + .catch((err) => { + console.log(err); + next(err) + }); +} + +function verifyEmailSchema(req, res, next) { + const schema = Joi.object({ + token: Joi.string().required() + }); + validateRequest(req, next, schema); +} + +function verifyEmail(req, res, next) { + accountService.verifyEmail(req.body) + .then(() => res.json({ message: 'Verification successful, you can now login' })) + .catch(next); +} + +function forgotPasswordSchema(req, res, next) { + const schema = Joi.object({ + email: Joi.string().email().required() + }); + validateRequest(req, next, schema); +} + +function forgotPassword(req, res, next) { + accountService.forgotPassword(req.body, req.get('origin')) + .then(() => res.json({ message: 'Please check your email for password reset instructions' })) + .catch(next); +} + +function validateResetTokenSchema(req, res, next) { + const schema = Joi.object({ + token: Joi.string().required() + }); + validateRequest(req, next, schema); +} + +function validateResetToken(req, res, next) { + accountService.validateResetToken(req.body) + .then(() => res.json({ message: 'Token is valid' })) + .catch(next); +} + +function resetPasswordSchema(req, res, next) { + const schema = Joi.object({ + token: Joi.string().required(), + password: Joi.string().min(6).required(), + confirmPassword: Joi.string().valid(Joi.ref('password')).required() + }); + validateRequest(req, next, schema); +} + +function resetPassword(req, res, next) { + accountService.resetPassword(req.body) + .then(() => res.json({ message: 'Password reset successful, you can now login' })) + .catch(next); +} diff --git a/app.js b/app.js index 1e730f5..91ffacb 100644 --- a/app.js +++ b/app.js @@ -1,13 +1,13 @@ var http = require('http'), - path = require('path'), - methods = require('methods'), - express = require('express'), - bodyParser = require('body-parser'), - session = require('express-session'), - cors = require('cors'), - passport = require('passport'), - errorhandler = require('errorhandler'), - mongoose = require('mongoose'); + path = require('path'), + methods = require('methods'), + express = require('express'), + bodyParser = require('body-parser'), + session = require('express-session'), + cors = require('cors'), + passport = require('passport'), + errorhandler = require('errorhandler'), + mongoose = require('mongoose'); var isProduction = process.env.NODE_ENV === 'production'; @@ -24,13 +24,13 @@ app.use(bodyParser.json()); app.use(require('method-override')()); app.use(express.static(__dirname + '/public')); -app.use(session({ secret: 'conduit', cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false })); +app.use(session({ secret: 'obsobs', cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false })); if (!isProduction) { app.use(errorhandler()); } -if(isProduction){ +if (isProduction) { mongoose.connect('mongodb://localhost/obs'); } else { mongoose.connect('mongodb://localhost/obsTest'); @@ -46,7 +46,7 @@ require('./config/passport'); app.use(require('./routes')); /// catch 404 and forward to error handler -app.use(function(req, res, next) { +app.use(function (req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); @@ -57,29 +57,33 @@ app.use(function(req, res, next) { // development error handler // will print stacktrace if (!isProduction) { - app.use(function(err, req, res, next) { + app.use(function (err, req, res, next) { console.log(err.stack); res.status(err.status || 500); - res.json({'errors': { - message: err.message, - error: err - }}); + res.json({ + 'errors': { + message: err.message, + error: err + } + }); }); } // production error handler // no stacktraces leaked to user -app.use(function(err, req, res, next) { +app.use(function (err, req, res, next) { res.status(err.status || 500); - res.json({'errors': { - message: err.message, - error: {} - }}); + res.json({ + 'errors': { + message: err.message, + error: {} + } + }); }); // finally, let's start our server... -var server = app.listen( process.env.PORT || 3000, function(){ +var server = app.listen(process.env.PORT || 3000, function () { console.log('Listening on port ' + server.address().port); }); diff --git a/config/email.js b/config/email.js new file mode 100644 index 0000000..38884d6 --- /dev/null +++ b/config/email.js @@ -0,0 +1,15 @@ +const isProduction = process.env.NODE_ENV === 'production'; +const forcedMail = process.argv.findIndex(s => s === '--devSendMails') !== -1; + +module.exports = { + "sendMails": isProduction || forcedMail, + "emailFrom": "noreply@openbikesensor.org", + "smtpOptions": { + "host": "mail.your-server.de", + "port": 587, + "auth": { + "user": process.env.MAILUSER, + "pass": process.env.MAILPW + } + } +}; \ No newline at end of file diff --git a/config/passport.js b/config/passport.js index abe0ce2..4150cc9 100644 --- a/config/passport.js +++ b/config/passport.js @@ -6,10 +6,14 @@ var User = mongoose.model('User'); passport.use(new LocalStrategy({ usernameField: 'user[email]', passwordField: 'user[password]' -}, function(email, password, done) { - User.findOne({email: email}).then(function(user){ - if(!user || !user.validPassword(password)){ - return done(null, false, {errors: {'email or password': 'is invalid'}}); +}, function (email, password, done) { + User.findOne({ email: email }).then(function (user) { + if (!user || !user.validPassword(password)) { + return done(null, false, { errors: { 'email or password': 'is invalid' } }); + } + + if (user && user.needsEmailValidation) { + return done(null, false, { errors: { 'E-Mail-Bestätigung': 'noch nicht erfolgt' } }); } return done(null, user); diff --git a/models/User.js b/models/User.js index 1b94331..9d7fb6f 100644 --- a/models/User.js +++ b/models/User.js @@ -5,30 +5,36 @@ var jwt = require('jsonwebtoken'); var secret = require('../config').secret; var UserSchema = new mongoose.Schema({ - username: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true}, - email: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true}, + username: { type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true }, + email: { type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true }, bio: String, image: String, favorites: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Article' }], following: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], areTracksVisibleForAll: Boolean, hash: String, - salt: String -}, {timestamps: true}); + salt: String, + needsEmailValidation: Boolean, + verificationToken: String, + resetToken: { + token: String, + expires: Date + } +}, { timestamps: true }); -UserSchema.plugin(uniqueValidator, {message: 'is already taken.'}); +UserSchema.plugin(uniqueValidator, { message: 'ist bereits vergeben. Sorry!' }); -UserSchema.methods.validPassword = function(password) { +UserSchema.methods.validPassword = function (password) { var hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex'); return this.hash === hash; }; -UserSchema.methods.setPassword = function(password){ +UserSchema.methods.setPassword = function (password) { this.salt = crypto.randomBytes(16).toString('hex'); this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex'); }; -UserSchema.methods.generateJWT = function() { +UserSchema.methods.generateJWT = function () { var today = new Date(); var exp = new Date(today); exp.setDate(today.getDate() + 60); @@ -40,7 +46,7 @@ UserSchema.methods.generateJWT = function() { }, secret); }; -UserSchema.methods.toAuthJSON = function(){ +UserSchema.methods.toAuthJSON = function () { return { username: this.username, email: this.email, @@ -52,7 +58,7 @@ UserSchema.methods.toAuthJSON = function(){ }; }; -UserSchema.methods.toProfileJSONFor = function(user){ +UserSchema.methods.toProfileJSONFor = function (user) { return { username: this.username, bio: this.bio, @@ -61,45 +67,45 @@ UserSchema.methods.toProfileJSONFor = function(user){ }; }; -UserSchema.methods.favorite = function(id){ - if(this.favorites.indexOf(id) === -1){ +UserSchema.methods.favorite = function (id) { + if (this.favorites.indexOf(id) === -1) { this.favorites.push(id); } return this.save(); }; -UserSchema.methods.unfavorite = function(id){ +UserSchema.methods.unfavorite = function (id) { this.favorites.remove(id); return this.save(); }; -UserSchema.methods.isFavorite = function(id){ - return this.favorites.some(function(favoriteId){ +UserSchema.methods.isFavorite = function (id) { + return this.favorites.some(function (favoriteId) { return favoriteId.toString() === id.toString(); }); }; -UserSchema.methods.isTrackVisible = function(id){ +UserSchema.methods.isTrackVisible = function (id) { return this.areTracksVisibleForAll(); }; -UserSchema.methods.follow = function(id){ - if(this.following.indexOf(id) === -1){ +UserSchema.methods.follow = function (id) { + if (this.following.indexOf(id) === -1) { this.following.push(id); } return this.save(); }; -UserSchema.methods.unfollow = function(id){ +UserSchema.methods.unfollow = function (id) { this.following.remove(id); return this.save(); }; -UserSchema.methods.isFollowing = function(id){ - return this.following.some(function(followId){ +UserSchema.methods.isFollowing = function (id) { + return this.following.some(function (followId) { return followId.toString() === id.toString(); }); }; diff --git a/package-lock.json b/package-lock.json index 60e48e8..1067256 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,37 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@hapi/address": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.1.0.tgz", + "integrity": "sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@hapi/formula": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz", + "integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==" + }, + "@hapi/hoek": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.0.tgz", + "integrity": "sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==" + }, + "@hapi/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==" + }, + "@hapi/topo": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@postman/form-data": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.0.tgz", @@ -1416,6 +1447,18 @@ } } }, + "joi": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.2.1.tgz", + "integrity": "sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA==", + "requires": { + "@hapi/address": "^4.1.0", + "@hapi/formula": "^2.0.0", + "@hapi/hoek": "^9.0.0", + "@hapi/pinpoint": "^2.0.0", + "@hapi/topo": "^5.0.0" + } + }, "js-sha512": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", @@ -1916,6 +1959,11 @@ "integrity": "sha512-0yggixNfrA1KcBwvh/Hy2xAS1Wfs9dcg6TdFf2zN7gilcAigMdrtZ4ybrBSXBgLvGDw9V1p2MRnGBMq7XjTWLg==", "dev": true }, + "nodemailer": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.14.tgz", + "integrity": "sha512-0AQHOOT+nRAOK6QnksNaK7+5vjviVvEBzmZytKU7XSA+Vze2NLykTx/05ti1uJgXFTWrMq08u3j3x4r4OE6PAA==" + }, "nodemon": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz", diff --git a/package.json b/package.json index 7f59547..946599b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "express": "4.17.1", "express-jwt": "^6.0.0", "express-session": "1.17.1", + "joi": "^17.2.1", "jsonwebtoken": "8.5.1", "latest": "^0.2.0", "method-override": "3.0.0", @@ -29,6 +30,7 @@ "mongoose": "^5.10.7", "mongoose-unique-validator": "2.0.3", "morgan": "1.10.0", + "nodemailer": "^6.4.14", "passport": "0.4.1", "passport-local": "1.0.0", "request": "2.88.2", @@ -39,4 +41,4 @@ "newman": "^5.2.0", "nodemon": "^2.0.4" } -} \ No newline at end of file +} diff --git a/project-logo.png b/project-logo.png deleted file mode 100644 index ef92a4b..0000000 Binary files a/project-logo.png and /dev/null differ diff --git a/routes/api/index.js b/routes/api/index.js index 6970fab..e33ca1f 100644 --- a/routes/api/index.js +++ b/routes/api/index.js @@ -4,11 +4,12 @@ router.use('/', require('./users')); router.use('/profiles', require('./profiles')); router.use('/tracks', require('./tracks')); router.use('/tags', require('./tags')); +router.use('/accounts', require('../../accounts/accounts.controller')); -router.use(function(err, req, res, next){ - if(err.name === 'ValidationError'){ +router.use(function (err, req, res, next) { + if (err.name === 'ValidationError') { return res.status(422).json({ - errors: Object.keys(err.errors).reduce(function(errors, key){ + errors: Object.keys(err.errors).reduce(function (errors, key) { errors[key] = err.errors[key].message; return errors; diff --git a/routes/api/users.js b/routes/api/users.js index bbaa913..a2f254f 100644 --- a/routes/api/users.js +++ b/routes/api/users.js @@ -4,75 +4,63 @@ var passport = require('passport'); var User = mongoose.model('User'); var auth = require('../auth'); -router.get('/user', auth.required, function(req, res, next){ - User.findById(req.payload.id).then(function(user){ - if(!user){ return res.sendStatus(401); } +router.get('/user', auth.required, function (req, res, next) { + User.findById(req.payload.id).then(function (user) { + if (!user) { return res.sendStatus(401); } - return res.json({user: user.toAuthJSON()}); + return res.json({ user: user.toAuthJSON() }); }).catch(next); }); -router.put('/user', auth.required, function(req, res, next){ - User.findById(req.payload.id).then(function(user){ - if(!user){ return res.sendStatus(401); } +router.put('/user', auth.required, function (req, res, next) { + User.findById(req.payload.id).then(function (user) { + if (!user) { return res.sendStatus(401); } // only update fields that were actually passed... - if(typeof req.body.user.username !== 'undefined'){ + if (typeof req.body.user.username !== 'undefined') { user.username = req.body.user.username; } - if(typeof req.body.user.email !== 'undefined'){ + if (typeof req.body.user.email !== 'undefined') { user.email = req.body.user.email; } - if(typeof req.body.user.bio !== 'undefined'){ + if (typeof req.body.user.bio !== 'undefined') { user.bio = req.body.user.bio; } - if(typeof req.body.user.image !== 'undefined'){ + if (typeof req.body.user.image !== 'undefined') { user.image = req.body.user.image; } - if(typeof req.body.user.areTracksVisibleForAll !== 'undefined'){ + if (typeof req.body.user.areTracksVisibleForAll !== 'undefined') { user.areTracksVisibleForAll = req.body.user.areTracksVisibleForAll; } - if(typeof req.body.user.password !== 'undefined' && req.body.user.password !='' ){ + if (typeof req.body.user.password !== 'undefined' && req.body.user.password != '') { user.setPassword(req.body.user.password); } - return user.save().then(function(){ - return res.json({user: user.toAuthJSON()}); + return user.save().then(function () { + return res.json({ user: user.toAuthJSON() }); }); }).catch(next); }); -router.post('/users/login', function(req, res, next){ - if(!req.body.user.email){ - return res.status(422).json({errors: {email: "can't be blank"}}); +router.post('/users/login', function (req, res, next) { + if (!req.body.user.email) { + return res.status(422).json({ errors: { email: "can't be blank" } }); } - if(!req.body.user.password){ - return res.status(422).json({errors: {password: "can't be blank"}}); + if (!req.body.user.password) { + return res.status(422).json({ errors: { password: "can't be blank" } }); } - passport.authenticate('local', {session: false}, function(err, user, info){ - if(err){ return next(err); } + passport.authenticate('local', { session: false }, function (err, user, info) { + if (err) { return next(err); } - if(user){ + if (user) { user.token = user.generateJWT(); - return res.json({user: user.toAuthJSON()}); + return res.json({ user: user.toAuthJSON() }); } else { return res.status(422).json(info); } })(req, res, next); }); -router.post('/users', function(req, res, next){ - var user = new User(); - - user.username = req.body.user.username; - user.email = req.body.user.email; - user.setPassword(req.body.user.password); - - user.save().then(function(){ - return res.json({user: user.toAuthJSON()}); - }).catch(next); -}); - module.exports = router;