diff --git a/task5/app/authenticate/index.js b/task5/app/authenticate/index.js new file mode 100644 index 0000000..961fb02 --- /dev/null +++ b/task5/app/authenticate/index.js @@ -0,0 +1,19 @@ +'use strict'; + +const Author = require('../models/author.js'); + +module.exports = (passport) => { + + passport.serializeUser(function(user, done) { + done(null, user._id); + }); + + passport.deserializeUser(function(id, done) { + Author.findById(id, function(err, user) { + done(err, user); + }); + }); + + require('./login')(passport); + require('./signup')(passport); +}; diff --git a/task5/app/authenticate/login.js b/task5/app/authenticate/login.js new file mode 100644 index 0000000..d01d345 --- /dev/null +++ b/task5/app/authenticate/login.js @@ -0,0 +1,31 @@ +'use strict'; + +const LocalStrategy = require('passport-local').Strategy; +const bcrypt = require('bcrypt-nodejs'); + +const Author = require('../models/author.js'); + +module.exports = (passport) => { + passport.use('login', new LocalStrategy({ + passReqToCallback: true + }, + function(req, username, password, done) { + Author.findOne({ 'username' : username }, + (err, user) => { + if (err) { + return done(err); + } + + if (!user) { + return done(null, false); + } + + if (!bcrypt.compareSync(password, user.password)) { + return done(null, false); + } + + return done(null, user); + } + ); + })); +}; diff --git a/task5/app/authenticate/signup.js b/task5/app/authenticate/signup.js new file mode 100644 index 0000000..b5eb1c0 --- /dev/null +++ b/task5/app/authenticate/signup.js @@ -0,0 +1,42 @@ +'use strict'; + +const LocalStrategy = require('passport-local').Strategy; + +const Author = require('../models/author.js'); + +module.exports = function (passport) { + passport.use('signup', new LocalStrategy({ + passReqToCallback: true + }, + function(req, username, password, done) { + process.nextTick(function () { + let email = req.param('email'); + Author.findOne({$or: [{'username': username} , {'email': email}]}, function (err, user) { + if (err) { + return done(err); + } + + if (user) { + let message = `User with username '${username}' already exists`; + if (user.username != username) { + message = `User with email '${email}' already exists`; + } + return done(null, false, {message}); + } + + let newUser = new Author(); + newUser.username = username; + newUser.password = password; + newUser.email = email; + + newUser.save(function (err) { + if (err) { + throw err; + } + return done(null, true); + }); + }); + }); + }) + ); +}; diff --git a/task5/app/config/dbConf.js b/task5/app/config/dbConf.js new file mode 100644 index 0000000..7d178dc --- /dev/null +++ b/task5/app/config/dbConf.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + dbName: 'heroku_x4cvh20w', + username: process.env.MONGO_USER, + password: process.env.MONGO_PASSWORD, + uri: 'ds133348.mlab.com', + port: 33348 +} diff --git a/task5/app/config/restifyConf.js b/task5/app/config/restifyConf.js new file mode 100644 index 0000000..448ed06 --- /dev/null +++ b/task5/app/config/restifyConf.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = { + prefix: '', + runValidators: true +}; diff --git a/task5/app/models/article.js b/task5/app/models/article.js new file mode 100644 index 0000000..72ea5b8 --- /dev/null +++ b/task5/app/models/article.js @@ -0,0 +1,29 @@ +'use strict'; + +const mongoose = require('../services/dbService'); +const Schema = mongoose.Schema; + +const ArticleSchema = new Schema({ + title: String, + body: String, + author: { + email: String, + _id: String + }, + cratedAt: Date, + updatedAt: Date +}); + +ArticleSchema.pre('save', function(next) { + if (this.isNew) { + this.cratedAt = new Date(); + } + this.updatedAt = new Date(); + next(); +}); + +ArticleSchema.pre('update', function() { + this.update({}, {$set: {updatedAt: new Date()}}); +}); + +module.exports = mongoose.model('Article', ArticleSchema); diff --git a/task5/app/models/author.js b/task5/app/models/author.js new file mode 100644 index 0000000..18af4c5 --- /dev/null +++ b/task5/app/models/author.js @@ -0,0 +1,44 @@ +'use strict'; + +const mongoose = require('../services/dbService'); +const bcrypt = require('bcrypt-nodejs'); +const Schema = mongoose.Schema; + +const AuthorSchema = new Schema({ + username: { + type: String, + required: true, + min: [3, 'Username should >=3'], + index: { + unique: true + } + }, + password: { + type: String, + required: true, + min: [6, 'Password should >=6'], + validate: { + validator: (v) => { + /^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]*$/.test(v); + }, + message: 'Password is not strong.' + } + }, + email: { + type: String, + index: { + unique: true + } + }, + isAdmin: { + type: Boolean, + default: false + } +}); + +AuthorSchema.pre('save', function(next) { + this.password = bcrypt.hashSync(this.password, bcrypt.genSaltSync(10), null); + next(); +}); + +module.exports = mongoose.model('Author', AuthorSchema); diff --git a/task5/app/routes/articleRouter.js b/task5/app/routes/articleRouter.js new file mode 100644 index 0000000..20b1940 --- /dev/null +++ b/task5/app/routes/articleRouter.js @@ -0,0 +1,17 @@ +'use strict'; + +const restify = require('express-restify-mongoose'); +const Article = require('../models/article'); + +let conf = require('../config/restifyConf'); +conf.preCreate = function(req, res, next) { + req.body.author = { + email: req.user.email, + _id: req.user._id + } + next(); +} + +module.exports = (router) => { + return restify.serve(router, Article, conf); +} diff --git a/task5/app/routes/authRouter.js b/task5/app/routes/authRouter.js new file mode 100644 index 0000000..ce419c1 --- /dev/null +++ b/task5/app/routes/authRouter.js @@ -0,0 +1,50 @@ +'use strict'; + +const isAuthenticated = require('../services/authService.js').isAuthenticated; + +module.exports = (router, passport) => { + + router.post('/login', (req, res, next) => { + passport.authenticate('login', (err, user, info) => { + if (err) { + res.statusCode = 500; + return res.end(); + } + + if (!user) { + res.statusCode = 400; + return res.json({'err': 'Wrong username or password'}); + } + + req.logIn(user, function(err) { + if (err) { + return next(err); + } + + res.statusCode = 200; + return res.json({email: user.email}); + }); + })(req, res, next); + }); + + router.post('/signup', (req, res, next) => { + passport.authenticate('signup', (err, isSuccess, info) => { + if (err) { + return res.sendStatus(500); + } + + if (!isSuccess) { + res.statusCode = 400; + return res.json(info); + } + + return res.sendStatus(200); + })(req, res, next); + }); + + router.get('/signout', isAuthenticated, function(req, res) { + req.logout(); + return res.sendStatus(200); + }); + +}; diff --git a/task5/app/routes/authorRouter.js b/task5/app/routes/authorRouter.js new file mode 100644 index 0000000..8b278f7 --- /dev/null +++ b/task5/app/routes/authorRouter.js @@ -0,0 +1,11 @@ +'use strict'; + +const restify = require('express-restify-mongoose'); +const Author = require('../models/author'); + +let conf = require('../config/restifyConf'); +conf.private = ['password']; + +module.exports = (router) => { + return restify.serve(router, Author, conf); +} diff --git a/task5/app/routes/index.js b/task5/app/routes/index.js new file mode 100644 index 0000000..e1ca33d --- /dev/null +++ b/task5/app/routes/index.js @@ -0,0 +1,17 @@ +'use strict'; + +const router = require('express').Router(); +const isAuthenticated = require('../services/authService.js').isAuthenticated; + +module.exports = (passport) => { + + router.all(/^\/v1\/.*$/, isAuthenticated, function(req, res, next){ + next(); + }) + + require('./authRouter')(router, passport); + require('./authorRouter')(router); + require('./articleRouter')(router); + + return router; +}; diff --git a/task5/app/services/authService.js b/task5/app/services/authService.js new file mode 100644 index 0000000..9f44eea --- /dev/null +++ b/task5/app/services/authService.js @@ -0,0 +1,11 @@ +'use strict'; + +const isAuthenticated = function (req, res, next) { + if (req.isAuthenticated()) + return next(); + res.sendStatus(401); +}; + +module.exports = { + isAuthenticated +} diff --git a/task5/app/services/dbService.js b/task5/app/services/dbService.js new file mode 100644 index 0000000..8311243 --- /dev/null +++ b/task5/app/services/dbService.js @@ -0,0 +1,20 @@ +'use strict'; + +const mongooseConf = require('../config/dbConf'); +const bluebird = require('bluebird'); + +let instance = require('mongoose'); +instance.connect( + `mongodb://${mongooseConf.username}:${mongooseConf.password}@${mongooseConf.uri}:${mongooseConf.port}/${mongooseConf.dbName}`, + { + promiseLibrary: bluebird + } +); + +class MongoConnection { + constructor() { + return instance; + } +} + +module.exports = new MongoConnection(); diff --git a/task5/index.js b/task5/index.js new file mode 100644 index 0000000..9b9e9f0 --- /dev/null +++ b/task5/index.js @@ -0,0 +1,38 @@ +'use strict'; + +const PORT = process.env.PORT || 3000; + +const express = require('express'); +const session = require('express-session'); +const bodyParser = require('body-parser'); +const flash = require('connect-flash'); + +const passport = require('passport'); +require('./app/authenticate')(passport); + +const app = express(); + +app + .use(bodyParser.urlencoded({ extended: true })) + .use(bodyParser.json()) + .use(session({ + resave: true, + secret: 'verySecretKey', + saveUninitialized: false + })) + .use(flash()) + .use(passport.initialize()) + .use(passport.session()) + +.use(function (req, res, next) { + res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); + res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); + res.setHeader('Access-Control-Allow-Credentials', true); + next(); +}); + +let router = require('./app/routes')(passport) +app.use('/api', router); + +app.listen(PORT); diff --git a/task5/package.json b/task5/package.json new file mode 100644 index 0000000..f42d6f8 --- /dev/null +++ b/task5/package.json @@ -0,0 +1,26 @@ +{ + "name": "nodeapp", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "mikita_siadykh@epam.com", + "license": "ISC", + "dependencies": { + "bcrypt-nodejs": "0.0.3", + "connect-flash": "0.1.1", + "flash": "1.1.0", + "passport-local": "1.0.0", + "bluebird": "3.4.7", + "body-parser": "1.15.2", + "express": "4.14.0", + "express-restify-mongoose": "4.1.3", + "express-session": "1.14.2", + "mongoose": "4.7.6", + "mongoose-auto-increment": "5.0.1", + "passport": "0.3.2", + "password": "0.1.1" + } +}