From c8dcdc96a2b7451199b67e3698eb14510dc893e2 Mon Sep 17 00:00:00 2001 From: dyankastutara Date: Tue, 25 Apr 2017 15:16:30 +0700 Subject: [PATCH 1/4] api-auth --- .gitignore | 1 + README.md | 30 +++++++- app.js | 46 ++++++++++++ bin/www | 90 +++++++++++++++++++++++ config/config.json | 23 ++++++ controllers/usersController.js | 87 ++++++++++++++++++++++ migrations/20170425041834-create-user.js | 36 +++++++++ migrations/20170425073213-addUserPass.js | 34 +++++++++ migrations/20170425073745-addAccess.js | 24 ++++++ models/index.js | 36 +++++++++ models/user.js | 19 +++++ package.json | 22 ++++++ public/stylesheets/style.css | 8 ++ routes/index.js | 9 +++ routes/users.js | 15 ++++ seeders/20170425042247-InsertDataDummy.js | 34 +++++++++ views/error.jade | 6 ++ views/index.jade | 5 ++ views/layout.jade | 7 ++ 19 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 app.js create mode 100644 bin/www create mode 100644 config/config.json create mode 100644 controllers/usersController.js create mode 100644 migrations/20170425041834-create-user.js create mode 100644 migrations/20170425073213-addUserPass.js create mode 100644 migrations/20170425073745-addAccess.js create mode 100644 models/index.js create mode 100644 models/user.js create mode 100644 package.json create mode 100644 public/stylesheets/style.css create mode 100644 routes/index.js create mode 100644 routes/users.js create mode 100644 seeders/20170425042247-InsertDataDummy.js create mode 100644 views/error.jade create mode 100644 views/index.jade create mode 100644 views/layout.jade diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md index b2e3b8b..0b816ae 100644 --- a/README.md +++ b/README.md @@ -1 +1,29 @@ -# api-auth \ No newline at end of file +# REST API Authentication + +Demo app with Authentication REST API + +## REST API + +List of user routes : + +| Route | HTTP | Description | +| -------------- | ------ | ---------------------------------------------------------- | +| /api/signup | POST | Sign up with new user info | +| /api/signin | POST | Sign in while get an access token based on credentials | +| /api/users | GET | Get All the user info (admin only) | +| /api/users/:id | GET | Get a single user (admin and authenticated user) | +| /api/users | POST | Create a user (admin only) | +| /api/users/:id | DELETE | Delete a user (admin only) | +| /api/users/:id | PUT | Update a user with new info (admin and authenticated user) | + + +## Usage + +With only npm: +> npm install + +> npm start + +> npm run dev + +Access the website via http://localhost:3000 or API via http://localhost:3000/api diff --git a/app.js b/app.js new file mode 100644 index 0000000..ace353d --- /dev/null +++ b/app.js @@ -0,0 +1,46 @@ +var express = require('express'); +var path = require('path'); +var favicon = require('serve-favicon'); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); + +var index = require('./routes/index'); +var users = require('./routes/users'); + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +// uncomment after placing your favicon in /public +//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + + +app.use('/', users); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// error handler +app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +module.exports = app; diff --git a/bin/www b/bin/www new file mode 100644 index 0000000..c0450cd --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('rest-api-basic:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..f3b7379 --- /dev/null +++ b/config/config.json @@ -0,0 +1,23 @@ +{ + "development": { + "username": "dyankastutara", + "password": "12345", + "database": "db_restful_api_basic", + "host": "127.0.0.1", + "dialect": "postgres" + }, + "test": { + "username": "root", + "password": null, + "database": "database_test", + "host": "127.0.0.1", + "dialect": "mysql" + }, + "production": { + "username": "root", + "password": null, + "database": "database_production", + "host": "127.0.0.1", + "dialect": "mysql" + } +} diff --git a/controllers/usersController.js b/controllers/usersController.js new file mode 100644 index 0000000..7a38c14 --- /dev/null +++ b/controllers/usersController.js @@ -0,0 +1,87 @@ +const models = require('../models'); +const passHash = require('password-hash'); + +var methode = {} + +methode.getAllData = function(req, res, next) { + models.User.findAll({}) + .then((query)=>{ + res.send(query) + }) +} ; + +methode.getDataById = function(req, res, next) { + models.User.findOne({ + where : { + id : req.params.id + } + }) + .then((query)=>{ + res.send(query) + }) +} ; + +methode.insert = (req, res, next)=>{ + models.User.create({ + firstname : req.body.firstname, + lastname : req.body.lastname, + dateofbirth : req.body.dateofbirth, + gender : req.body.gender, + username : req.body.username, + password : passHash.generate(req.body.password), + access : req.body.access || 'user' + }) + .then((query)=>{ + res.send(query) + }) +}; + +methode.delete = (req, res, next)=>{ + models.User.destroy({ + where :{ + id : req.params.id + } + }) + .then(()=>{ + res.send("Data Deleted with id : "+req.params.id) + }) +}; + +methode.updates = (req, res, next)=>{ + models.User.update({ + firstname : req.body.firstname, + lastname : req.body.lastname, + dateofbirth : req.body.dateofbirth, + gender : req.body.gender, + username : req.body.username, + password : passHash.generate(req.body.password), + access : req.body.access, + updatedAt : new Date() + },{ + where : { + id : req.params.id + } + }) + .then((query)=>{ + res.send(query) + }) +}; + +methode.login = (req, res, next)=>{ + models.User.findOne({ + where :{ + username : req.body.username + } + + }) + .then((query)=>{ + if(passHash.verify(req.body.password, query.password)){ + if(query.access === 'admin'){ + next() + } + }else{ + res.send('username or password is wrong') + } + }) +} +module.exports = methode diff --git a/migrations/20170425041834-create-user.js b/migrations/20170425041834-create-user.js new file mode 100644 index 0000000..8fac753 --- /dev/null +++ b/migrations/20170425041834-create-user.js @@ -0,0 +1,36 @@ +'use strict'; +module.exports = { + up: function(queryInterface, Sequelize) { + return queryInterface.createTable('Users', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + firstname: { + type: Sequelize.STRING + }, + lastname: { + type: Sequelize.STRING + }, + dateofbirth: { + type: Sequelize.DATE + }, + gender: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: function(queryInterface, Sequelize) { + return queryInterface.dropTable('Users'); + } +}; \ No newline at end of file diff --git a/migrations/20170425073213-addUserPass.js b/migrations/20170425073213-addUserPass.js new file mode 100644 index 0000000..4e8d857 --- /dev/null +++ b/migrations/20170425073213-addUserPass.js @@ -0,0 +1,34 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + return [ + queryInterface.addColumn( + 'Students', + 'name', { + type: Sequelize.STRING + } + ) + ]; + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return [ + queryInterface.addColumn('Users','username', {type : Sequelize.STRING}), + queryInterface.addColumn('Users','password', {type : Sequelize.STRING}) + ] + }, + + down: function (queryInterface, Sequelize) { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/migrations/20170425073745-addAccess.js b/migrations/20170425073745-addAccess.js new file mode 100644 index 0000000..6a02b4d --- /dev/null +++ b/migrations/20170425073745-addAccess.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + queryInterface.addColumn('Users','access', {type : Sequelize.STRING}) + }, + + down: function (queryInterface, Sequelize) { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/models/index.js b/models/index.js new file mode 100644 index 0000000..7540dba --- /dev/null +++ b/models/index.js @@ -0,0 +1,36 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var Sequelize = require('sequelize'); +var basename = path.basename(module.filename); +var env = process.env.NODE_ENV || 'development'; +var config = require(__dirname + '/../config/config.json')[env]; +var db = {}; + +if (config.use_env_variable) { + var sequelize = new Sequelize(process.env[config.use_env_variable]); +} else { + var sequelize = new Sequelize(config.database, config.username, config.password, config); +} + +fs + .readdirSync(__dirname) + .filter(function(file) { + return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); + }) + .forEach(function(file) { + var model = sequelize['import'](path.join(__dirname, file)); + db[model.name] = model; + }); + +Object.keys(db).forEach(function(modelName) { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..c126a14 --- /dev/null +++ b/models/user.js @@ -0,0 +1,19 @@ +'use strict'; +module.exports = function(sequelize, DataTypes) { + var User = sequelize.define('User', { + firstname: DataTypes.STRING, + lastname: DataTypes.STRING, + dateofbirth: DataTypes.DATE, + gender: DataTypes.STRING, + username : DataTypes.STRING, + password : DataTypes.STRING, + access : DataTypes.STRING + }, { + classMethods: { + associate: function(models) { + // associations can be defined here + } + } + }); + return User; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ef7951b --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "rest-api-basic", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "nodemon ./bin/www" + }, + "dependencies": { + "body-parser": "~1.17.1", + "cookie-parser": "~1.4.3", + "debug": "~2.6.3", + "express": "~4.15.2", + "jade": "~1.11.0", + "jsonwebtoken": "^7.4.0", + "morgan": "~1.8.1", + "password-hash": "^1.2.2", + "pg": "^6.1.5", + "sequelize": "^3.30.4", + "sequelize-cli": "^2.7.0", + "serve-favicon": "~2.4.2" + } +} diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 0000000..9453385 --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,8 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..ecca96a --- /dev/null +++ b/routes/index.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +module.exports = router; diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..1d82b47 --- /dev/null +++ b/routes/users.js @@ -0,0 +1,15 @@ +var express = require('express'); +var router = express.Router(); +var models = require('../models'); +var controller = require('../controllers/usersController'); + +/* GET users listing. */ +router.post('/api/signup', controller.insert); +router.post('/api/signin', controller.login); +router.get('/api/users', controller.getAllData); +router.get('/api/users/:id', controller.getDataById); +router.delete('/api/users/:id', controller.delete); +router.put('/api/users/:id',controller.updates); +router.patch('/api/users/:id',controller.updates); + +module.exports = router; diff --git a/seeders/20170425042247-InsertDataDummy.js b/seeders/20170425042247-InsertDataDummy.js new file mode 100644 index 0000000..09a35d3 --- /dev/null +++ b/seeders/20170425042247-InsertDataDummy.js @@ -0,0 +1,34 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('Person', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + return queryInterface.bulkInsert('Users', [{ + firstname : 'Dyan', + lastname : 'Kastutara', + dateofbirth : '1994-07-19', + gender : 'Man', + createdAt : new Date(), + updatedAt : new Date() + }]) + }, + + down: function (queryInterface, Sequelize) { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('Person', null, {}); + */ + } +}; diff --git a/views/error.jade b/views/error.jade new file mode 100644 index 0000000..51ec12c --- /dev/null +++ b/views/error.jade @@ -0,0 +1,6 @@ +extends layout + +block content + h1= message + h2= error.status + pre #{error.stack} diff --git a/views/index.jade b/views/index.jade new file mode 100644 index 0000000..3d63b9a --- /dev/null +++ b/views/index.jade @@ -0,0 +1,5 @@ +extends layout + +block content + h1= title + p Welcome to #{title} diff --git a/views/layout.jade b/views/layout.jade new file mode 100644 index 0000000..15af079 --- /dev/null +++ b/views/layout.jade @@ -0,0 +1,7 @@ +doctype html +html + head + title= title + link(rel='stylesheet', href='/stylesheets/style.css') + body + block content From 30a3014174ef67ed65da196ac5f11bff51c0f94c Mon Sep 17 00:00:00 2001 From: dyankastutara Date: Tue, 25 Apr 2017 16:17:38 +0700 Subject: [PATCH 2/4] password hash --- controllers/usersController.js | 67 ++++++++++++++++++++++++++-------- routes/users.js | 4 +- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/controllers/usersController.js b/controllers/usersController.js index 7a38c14..10e0d45 100644 --- a/controllers/usersController.js +++ b/controllers/usersController.js @@ -1,8 +1,59 @@ const models = require('../models'); const passHash = require('password-hash'); +const jwt = require('jsonwebtoken'); var methode = {} + +methode.signup = (req, res, next)=>{ + models.User.findOne({ + where : { + username : req.body.username + } + }) + .then ((query)=>{ + if(!query){ + models.User.create({ + firstname : req.body.firstname, + lastname : req.body.lastname, + dateofbirth : req.body.dateofbirth, + gender : req.body.gender, + username : req.body.username, + password : passHash.generate(req.body.password), + access : req.body.access || 'user' + }) + .then(()=>{ + res.send('User added') + }) + }else{ + res.send('Username already exists') + } + }) +}; + +methode.signin = (req, res, next)=>{ + models.User.findOne({ + where :{ + username : req.body.username + } + }) + .then((query)=>{ + if(passHash.verify(req.body.password, query.password)){ + var myToken = jwt.sign({username : query.username}, 'secret', {expiresIn : 1}); + res.send({ + 'token' : myToken, + 'user_id' : query.id, + 'username' : query.username, + 'name' : query.firstname+" "+query.lastname + }) + }else{ + res.send('username or password is wrong') + } + }) +} + + + methode.getAllData = function(req, res, next) { models.User.findAll({}) .then((query)=>{ @@ -67,21 +118,5 @@ methode.updates = (req, res, next)=>{ }) }; -methode.login = (req, res, next)=>{ - models.User.findOne({ - where :{ - username : req.body.username - } - }) - .then((query)=>{ - if(passHash.verify(req.body.password, query.password)){ - if(query.access === 'admin'){ - next() - } - }else{ - res.send('username or password is wrong') - } - }) -} module.exports = methode diff --git a/routes/users.js b/routes/users.js index 1d82b47..e18a801 100644 --- a/routes/users.js +++ b/routes/users.js @@ -4,8 +4,8 @@ var models = require('../models'); var controller = require('../controllers/usersController'); /* GET users listing. */ -router.post('/api/signup', controller.insert); -router.post('/api/signin', controller.login); +router.post('/api/signup', controller.signup); +router.post('/api/signin', controller.signin); router.get('/api/users', controller.getAllData); router.get('/api/users/:id', controller.getDataById); router.delete('/api/users/:id', controller.delete); From f28db8196d34168a6ee4654caf07d3194e3889a5 Mon Sep 17 00:00:00 2001 From: dyankastutara Date: Tue, 25 Apr 2017 17:45:11 +0700 Subject: [PATCH 3/4] api admin --- controllers/usersController.js | 6 ++++-- helper/jwthelper.js | 16 ++++++++++++++++ routes/users.js | 7 ++++--- 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 helper/jwthelper.js diff --git a/controllers/usersController.js b/controllers/usersController.js index 10e0d45..1e788e2 100644 --- a/controllers/usersController.js +++ b/controllers/usersController.js @@ -2,6 +2,7 @@ const models = require('../models'); const passHash = require('password-hash'); const jwt = require('jsonwebtoken'); + var methode = {} @@ -39,12 +40,13 @@ methode.signin = (req, res, next)=>{ }) .then((query)=>{ if(passHash.verify(req.body.password, query.password)){ - var myToken = jwt.sign({username : query.username}, 'secret', {expiresIn : 1}); + var myToken = jwt.sign({username : query.username, access:query.access}, 'secret', {expiresIn : '1h'}); res.send({ 'token' : myToken, 'user_id' : query.id, 'username' : query.username, - 'name' : query.firstname+" "+query.lastname + 'name' : query.firstname+" "+query.lastname, + 'access' : query.access }) }else{ res.send('username or password is wrong') diff --git a/helper/jwthelper.js b/helper/jwthelper.js new file mode 100644 index 0000000..18d0c86 --- /dev/null +++ b/helper/jwthelper.js @@ -0,0 +1,16 @@ +var jwt = require('jsonwebtoken'); +var methode ={} + + methode.check_token = (req, res, next) =>{ + jwt.verify(req.headers.token, 'secret', (err, decoded) =>{ + if(decoded){ + if(decoded.access === 'admin'){ + next(); + } + }else{ + res.send('tes') + } + }) + } + +module.exports = methode diff --git a/routes/users.js b/routes/users.js index e18a801..78448a7 100644 --- a/routes/users.js +++ b/routes/users.js @@ -2,14 +2,15 @@ var express = require('express'); var router = express.Router(); var models = require('../models'); var controller = require('../controllers/usersController'); +const jwtHelper = require('../helper/jwthelper'); /* GET users listing. */ router.post('/api/signup', controller.signup); router.post('/api/signin', controller.signin); -router.get('/api/users', controller.getAllData); +router.post('/api/users',jwtHelper.check_token, controller.insert); +router.get('/api/users',jwtHelper.check_token, controller.getAllData); router.get('/api/users/:id', controller.getDataById); -router.delete('/api/users/:id', controller.delete); +router.delete('/api/users/:id',jwtHelper.check_token, controller.delete); router.put('/api/users/:id',controller.updates); -router.patch('/api/users/:id',controller.updates); module.exports = router; From c505ca7633b55c6633aa1bf67a2320eef6f8e5f6 Mon Sep 17 00:00:00 2001 From: dyankastutara Date: Tue, 25 Apr 2017 18:06:20 +0700 Subject: [PATCH 4/4] api global --- controllers/usersController.js | 2 +- helper/jwthelper.js | 16 ++++++++++++++++ routes/users.js | 7 ++++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/controllers/usersController.js b/controllers/usersController.js index 1e788e2..892e733 100644 --- a/controllers/usersController.js +++ b/controllers/usersController.js @@ -61,7 +61,7 @@ methode.getAllData = function(req, res, next) { .then((query)=>{ res.send(query) }) -} ; +}; methode.getDataById = function(req, res, next) { models.User.findOne({ diff --git a/helper/jwthelper.js b/helper/jwthelper.js index 18d0c86..cb3564f 100644 --- a/helper/jwthelper.js +++ b/helper/jwthelper.js @@ -6,6 +6,22 @@ var methode ={} if(decoded){ if(decoded.access === 'admin'){ next(); + }else{ + res.send('You can not access') + } + }else{ + res.send('tes') + } + }) + } + + methode.check_token_global = (req, res, next) =>{ + jwt.verify(req.headers.token, 'secret', (err, decoded) =>{ + if(decoded){ + if(decoded.access === 'admin' || decoded.access === 'user' ){ + next(); + }else{ + res.send('You can not access') } }else{ res.send('tes') diff --git a/routes/users.js b/routes/users.js index 78448a7..ad9a489 100644 --- a/routes/users.js +++ b/routes/users.js @@ -7,10 +7,11 @@ const jwtHelper = require('../helper/jwthelper'); /* GET users listing. */ router.post('/api/signup', controller.signup); router.post('/api/signin', controller.signin); -router.post('/api/users',jwtHelper.check_token, controller.insert); router.get('/api/users',jwtHelper.check_token, controller.getAllData); -router.get('/api/users/:id', controller.getDataById); +router.post('/api/users',jwtHelper.check_token, controller.insert); + +router.get('/api/users/:id', jwtHelper.check_token_global,controller.getDataById); router.delete('/api/users/:id',jwtHelper.check_token, controller.delete); -router.put('/api/users/:id',controller.updates); +router.put('/api/users/:id',jwtHelper.check_token_global, controller.updates); module.exports = router;