diff --git a/.env b/.env new file mode 100644 index 0000000..a4d2901 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +SECRET_KEY=acmilan diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9daa824 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +node_modules diff --git a/README.md b/README.md index c1efb05..aba38a4 100644 --- a/README.md +++ b/README.md @@ -1 +1,35 @@ -# rest-api-auth \ No newline at end of file +# Database Hacktiv8 Students +Database hacktiv8 students with basic REST API + +## rest-api-crud +List of basic routes: + +| **Route** | **HTTP** | **Description** | +|-----------|----------|---------------------------------------| +| / | GET | Print "Welcome to Hacktiv8 database!" | + +List of user routes: + +| **Route** | **HTTP** | **Description** | +|--------------------|----------|------------------------------------------------------------| +| /api/users | GET | Get all the users info (admin only) | +| /api/users/:id | GET | Get a single user info (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) | + +List of user signin and signup: + +| **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 | + +## Usage +With only npm: +``` +npm install +npm start +``` +Access the website via http://localhost:3000 or API via http://localhost:3000/api/users. +Access the website via https://raynor-rest-auth.herokuapp.com diff --git a/app.js b/app.js new file mode 100644 index 0000000..6ca5922 --- /dev/null +++ b/app.js @@ -0,0 +1,36 @@ +var express = require('express'); +var bodyParser = require('body-parser'); + +var index = require('./routes/index'); +var users = require('./routes/api/users'); +var sign = require('./routes/api/index'); + +var app = express(); + +// uncomment after placing your favicon in /public +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); + +app.use('/', index); +app.use('/api/users', users); +app.use('/api', sign); + +// 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) { + // 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 100755 index 0000000..e851ea6 --- /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-crud: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..d44c46f --- /dev/null +++ b/config/config.json @@ -0,0 +1,13 @@ +{ + "development": { + "username": "postgres", + "password": "jack1899", + "database": "hacktiv8", + "host": "127.0.0.1", + "port": "5432", + "dialect": "postgres" + }, + "production": { + "use_env_variable": "DATABASE_URL" + } +} diff --git a/controllers/users_controller.js b/controllers/users_controller.js new file mode 100644 index 0000000..7edc4d5 --- /dev/null +++ b/controllers/users_controller.js @@ -0,0 +1,111 @@ +require('dotenv').config(); +const db = require('../models'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +let secret = process.env.SECRET_KEY; + +function getAllUsers(req, res) { + db.Students.findAll({ + order: "id ASC" + }) + .then(student => res.send(student)) + .catch(err => res.send(err.message)); +} + +function getSingleUser(req, res) { + let id = req.params.id; + db.Students.findById(id) + .then(student => res.send(student)) + .catch(err => res.send(err.message)); +} + +function createUser(req, res) { + let hash = bcrypt.hashSync(req.body.password, 8); + + db.Students.create({ + name : req.body.name, + gender : req.body.gender, + age : req.body.age, + address : req.body.address, + email : req.body.email, + username : req.body.username, + password : hash, + role : req.body.role + }) + .then(() => res.send(`Create user success!!`)) + .catch(err => res.send(err.message)); +} + +function deleteUser(req, res) { + db.Students.destroy({ + where : { + id : req.params.id + } + }) + .then(() => res.send('Delete user success!!')) + .catch(err => res.send(err.message)); +} + +function updateUser(req, res) { + let hash = bcrypt.hashSync(req.body.password, 8); + + db.Students.findById(req.params.id) + .then(student => { + db.Students.update({ + name : req.body.name || student.name, + gender : req.body.gender || student.gender, + age : req.body.age || student.age, + address : req.body.address || student.address, + email : req.body.email || student.email, + username : req.body.username || student.username, + password : hash || student.password, + role : req.body.role || student.role + }, { + where: { + id: req.params.id + } + }) + res.send(`Update user success!!`); + }) + .catch(err => res.send(err.message)); +} + +function signUp(req, res) { + let hash = bcrypt.hashSync(req.body.password, 8); + + db.Students.create({ + name : req.body.name, + gender : req.body.gender, + age : req.body.age, + address : req.body.address, + email : req.body.email, + username : req.body.username, + password : hash, + role : req.body.role + }) + .then(() => res.send(`Create user success!!`)) + .catch(err => res.send(err.message)); +} + +function signIn(req, res) { + db.Students.find({ + where: { + username : req.body.username + } + }) + .then(user => { + bcrypt.compare(req.body.password, user.password, function(err, result) { + if(result) { + let token = jwt.sign({role: user.role, id: user.id}, secret); + res.send(token); + } else { + res.send("Wrong password..") + } + }) + }) + .catch(err => res.send(err.message)); +} + +module.exports = { + getAllUsers, getSingleUser, createUser, deleteUser, updateUser, signUp, signIn +}; diff --git a/helpers/util.js b/helpers/util.js new file mode 100644 index 0000000..58915d5 --- /dev/null +++ b/helpers/util.js @@ -0,0 +1,39 @@ +require('dotenv').config(); +let sec = process.env.SECRET_KEY; +var jwt = require('jsonwebtoken'); + +function admin(req, res, next) { + let token = req.headers.token + + if(token) { + jwt.verify(token, process.env.SECRET_KEY, (err, decoded) => { + if(decoded.role == 'admin') { + next() + } else { + res.send('This route for admin only') + } + }) + } else { + res.send('Please login first!') + } +} + +function auth(req, res, next) { + let token = req.headers.token + + if(token) { + jwt.verify(token, sec, (err, decoded) => { + if(decoded.role == 'admin' || decoded.id == req.params.id) { + next() + } else { + res.send('This route for admin and authenticated user only') + } + }) + } else { + res.send('Please login first!') + } +} + +module.exports = { + admin, auth +}; diff --git a/migrations/20170522054555-create-students.js b/migrations/20170522054555-create-students.js new file mode 100644 index 0000000..949c8d1 --- /dev/null +++ b/migrations/20170522054555-create-students.js @@ -0,0 +1,39 @@ +'use strict'; +module.exports = { + up: function(queryInterface, Sequelize) { + return queryInterface.createTable('Students', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING + }, + gender: { + type: Sequelize.STRING + }, + age: { + type: Sequelize.INTEGER + }, + address: { + type: Sequelize.STRING + }, + email: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: function(queryInterface, Sequelize) { + return queryInterface.dropTable('Students'); + } +}; diff --git a/migrations/20170522105221-add.js b/migrations/20170522105221-add.js new file mode 100644 index 0000000..d79337a --- /dev/null +++ b/migrations/20170522105221-add.js @@ -0,0 +1,27 @@ +'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 }); + */ + return [queryInterface.addColumn('Students','username',Sequelize.STRING), + queryInterface.addColumn('Students','password',Sequelize.STRING), + queryInterface.addColumn('Students','role',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/students.js b/models/students.js new file mode 100644 index 0000000..d41e7c4 --- /dev/null +++ b/models/students.js @@ -0,0 +1,43 @@ +'use strict'; +module.exports = function(sequelize, DataTypes) { + var Students = sequelize.define('Students', { + name: DataTypes.STRING, + gender: DataTypes.STRING, + age: DataTypes.INTEGER, + address: DataTypes.STRING, + email: { + type: DataTypes.STRING, + validate: { + isEmail: true, + isUnique: function(value, next) { + Students.find({ + where: {email: value}, + attributes: ['id'] + }) + .done(function(error, user) { + if (error) + // Some unexpected error occured with the find method. + return next('Email address already in use!'); + if (user) + // We found a user with this email address. + // Pass the error to the next method. + return next('Email address already in use!'); + // If we got this far, the email address hasn't been used yet. + // Call next with no arguments when validation is successful. + next(); + }); + } + } + }, + username: DataTypes.STRING, + password: DataTypes.STRING, + role: DataTypes.STRING + }, { + classMethods: { + associate: function(models) { + // associations can be defined here + } + } + }); + return Students; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..15f477f --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "rest-api-crud", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "bcrypt": "^1.0.2", + "body-parser": "~1.17.1", + "cookie-parser": "~1.4.3", + "debug": "~2.6.3", + "dotenv": "^4.0.0", + "express": "~4.15.2", + "jade": "~1.11.0", + "jsonwebtoken": "^7.4.1", + "morgan": "~1.8.1", + "pg": "^6.2.2", + "pg-hstore": "^2.3.2", + "sequelize": "^3.30.4", + "sequelize-cli": "^2.7.0", + "serve-favicon": "~2.4.2" + }, + "description": "database gray-fox", + "main": "app.js", + "devDependencies": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/raynormw/rest-api-crud.git" + }, + "keywords": [ + "database", + "hacktiv8", + "crud" + ], + "author": "raynormw", + "license": "ISC", + "bugs": { + "url": "https://github.com/raynormw/rest-api-crud/issues" + }, + "homepage": "https://github.com/raynormw/rest-api-crud#readme" +} diff --git a/routes/api/index.js b/routes/api/index.js new file mode 100644 index 0000000..9f05197 --- /dev/null +++ b/routes/api/index.js @@ -0,0 +1,8 @@ +var express = require('express'); +var router = express.Router(); +var usersController = require('../../controllers/users_controller'); + +router.post('/signup', usersController.signUp); +router.post('/signin', usersController.signIn); + +module.exports = router; diff --git a/routes/api/users.js b/routes/api/users.js new file mode 100644 index 0000000..be96753 --- /dev/null +++ b/routes/api/users.js @@ -0,0 +1,13 @@ +var express = require('express'); +var router = express.Router(); +var usersController = require('../../controllers/users_controller'); +var helpers = require('../../helpers/util'); + +/* GET users listing. */ +router.get('/', helpers.admin, usersController.getAllUsers); +router.get('/:id', helpers.auth, usersController.getSingleUser); +router.post('/', helpers.admin, usersController.createUser); +router.delete('/:id', helpers.admin, usersController.deleteUser); +router.put('/:id', helpers.auth, usersController.updateUser); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..f9acf6d --- /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) { + res.send('Welcome to Hacktiv8 database! Click for usage and info..'); +}); + +module.exports = router; diff --git a/seeders/20170522054900-unnamed-seeder.js b/seeders/20170522054900-unnamed-seeder.js new file mode 100644 index 0000000..b3bda88 --- /dev/null +++ b/seeders/20170522054900-unnamed-seeder.js @@ -0,0 +1,49 @@ +'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('Students', [{ + name: 'Tirta Wirya Putra', + gender: 'Male', + age: 29, + address: 'Jl.K.H Hasyim Ashari, Cipondoh - Tangerang', + email: 'tirtawiryaputra@yahoo.com', + username: "admin", + password: "admin", + role: "admin", + createdAt: new Date(), + updatedAt: new Date() + }, { + name: 'Erwin', + gender: 'Male', + age: 33, + address: 'Jl.Tegal Rotan, Pondok Aren - Tangerang Selatan', + email: 'erwin_mencret_di@celana.com', + username: "erwin", + password: "erwin", + role: "user", + 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, {}); + */ + } +};