Created
May 1, 2018 21:58
-
-
Save GeroSalas/74c6a504d377309495c2b9636501dd45 to your computer and use it in GitHub Desktop.
Simple API REST (NodeJS - Express - MongoDB)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Simple API REST (NodeJS - Express - MongoDB) | |
const express = require('express') | |
const app = express() | |
const bodyParser = require('body-parser') | |
const mongoose = require('mongoose') | |
const bcrypt = require('bcrypt') | |
const salt = bcrypt.genSaltSync(10) // fixed salt | |
// Tokens Blacklist (a very straightforward alternative to some STS) | |
const revokedTokens = new Set() | |
// MongoDB Schema | |
const userSchema = mongoose.Schema({ | |
name: String, | |
email: String, | |
password: String | |
}) | |
const User = mongoose.model('User', userSchema) | |
const albumSchema = mongoose.Schema({ | |
performer: String, | |
title: String, | |
cost: Number | |
}) | |
const Album = mongoose.model('Album', albumSchema) | |
const puchaseSchema = mongoose.Schema({ | |
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, | |
album: { type: mongoose.Schema.Types.ObjectId, ref: 'Album' } | |
}) | |
const Purchase = mongoose.model('Purchase', puchaseSchema) | |
// Util 'private' methods | |
const _atob = (str) => { | |
return Buffer.from(str).toString('base64') | |
} | |
const _btoa = (str) => { | |
return Buffer.from(str, 'base64').toString() | |
} | |
const _encrypt = (str) => { | |
return bcrypt.hashSync(str, salt) | |
} | |
const _validateHash = (source, target) => { | |
return bcrypt.compareSync(source, target) | |
} | |
// Some custom short-term access token (a very straightforward alternative to JWT) | |
const _generateAccessToken = ({ email, password }) => { | |
const claims = { iss: 'Test', iat: Date.now(), exp: Date.now() + 3600000, user: email } | |
const encodedPayload = _atob(JSON.stringify(claims)) // base64Encode(claims) --> Payload | |
const signature = _encrypt(encodedPayload) // base64Encode(payload) --> Signature | |
const token = `${encodedPayload}::${signature}` // Payload::Signature --> Token | |
console.log('Access token created', token) | |
return token | |
} | |
// Add token to blacklist | |
const _revokeAccessToken = (token) => { | |
console.log('Access token blacklisted', token) | |
revokedTokens.add(token) | |
} | |
// Custom token validation | |
const _validateToken = (token) => { | |
const payload = token.split('::')[0] | |
const claims = JSON.parse(_btoa(payload)) | |
const signature = token.split('::')[1] | |
const isValid = claims && claims.exp > Date.now() && _validateHash(payload, signature) && !revokedTokens.has(token) // can add some role filtering | |
return isValid | |
} | |
// Middlewares | |
const logger = (req, res, next) => { | |
console.log(`Requested endpoint: ${req.method} ${req.originalUrl}`) | |
next() | |
} | |
const errorHandler = (error, req, res, next) => { | |
console.error(`Error encountered ${error.message}`) | |
res.status(500).json({ error }) | |
} | |
const authFilter = (req, res, next) => { | |
console.log('Authenticating request...') | |
const auth = req.headers['authorization'] | |
if (auth && auth.split('::').length > 0 && _validateToken(auth)) { | |
console.log('Authorization passed ok...') | |
next() | |
} else { | |
return res.status(401).json({ message: 'Invalid access token' }) | |
} | |
} | |
// Express application configs | |
app.use(bodyParser.json()) | |
app.use(logger) | |
app.use(['/albums', '/purchases'], authFilter) | |
// Repository Methods | |
const getAlbums = () => { | |
return new Promise((resolve, reject) => { | |
const pageSize = 100 // if it scales should use some pagination | |
Album.find().limit(pageSize) | |
.then((data) => resolve(data)) | |
.catch((error) => reject(error)) | |
}) | |
} | |
const getAlbumById = (id) => { | |
return new Promise((resolve, reject) => { | |
Album.findById(id) | |
.then((data) => resolve(data)) | |
.catch((error) => reject(error)) | |
}) | |
} | |
const saveAlbum = (album) => { | |
return new Promise((resolve, reject) => { | |
new Album(album).save() | |
.then((data) => resolve(data)) | |
.catch((error) => reject(error)) | |
}) | |
} | |
const updateAlbum = (id, album) => { | |
return new Promise((resolve, reject) => { | |
Album.findByIdAndUpdate(id, album, { new: true }) | |
.then((data) => resolve(data)) | |
.catch((error) => reject(error)) | |
}) | |
} | |
const removeAlbum = (id) => { | |
return new Promise((resolve, reject) => { | |
Album.findByIdAndRemove(id) | |
.then((data) => resolve(data)) | |
.catch((error) => reject(error)) | |
}) | |
} | |
const savePurchase = (purchase) => { | |
return new Promise((resolve, reject) => { | |
new Purchase(purchase).save() | |
.then((data) => resolve(data)) | |
.catch((error) => reject(error)) | |
}) | |
} | |
const registerUser = (user) => { | |
return new Promise((resolve, reject) => { | |
new User(user).save() | |
.then((data) => resolve(data)) | |
.catch((error) => reject(error)) | |
}) | |
} | |
// API Routes (REST Endpoints) | |
// User registration | |
app.post('/signup', (req, res, next) => { | |
let user = req.body | |
console.log(`Registering new User with email ${user.email}`) | |
user.password = _encrypt(user.password) | |
registerUser(user) | |
.then((data) => { | |
const auth = _generateAccessToken(data) | |
res.header('authorization', auth).status(204).send() | |
}) | |
.catch((error) => next(error)) | |
}) | |
// User login | |
app.post('/login', (req, res) => { | |
const { email, password } = req.body | |
console.log(`Login requested by User email: ${email}`) | |
User.findOne({ email }, (err, data) => { | |
if (_validateHash(password, data.password)) { | |
console.log('logged!!!') | |
const auth = _generateAccessToken(data) | |
res.header('authorization', auth).status(204).send() | |
} else { | |
res.removeHeader('authorization') | |
res.status(401).send('Invalid Credentials') | |
} | |
}) | |
}) | |
// User logout | |
app.post('/logout', (req, res) => { | |
const auth = req.headers['authorization'] | |
console.log(`Logout requested: revoking access token ${auth}`) | |
_revokeAccessToken(auth) | |
res.removeHeader('authorization') | |
res.status(204).send() | |
}) | |
// Read all albums | |
app.get('/albums', (req, res, next) => { | |
console.log('Retrieving all albums...') | |
getAlbums() | |
.then((data) => res.json({ data })) | |
.catch((error) => next(error)) | |
}) | |
// Find Album by ID | |
app.get('/albums/:id', (req, res, next) => { | |
const albumId = req.params.id | |
console.log(`Retrieving album by ID ${albumId}`) | |
getAlbumById(albumId) | |
.then((data) => res.json({ data })) | |
.catch((error) => next(error)) | |
}) | |
// Create Album | |
app.post('/albums', (req, res, next) => { | |
const album = req.body | |
console.log('Creating new album...') | |
saveAlbum(album) | |
.then((data) => res.json({ data })) | |
.catch((error) => next(error)) | |
}) | |
// Update Album | |
app.put('/albums/:id', (req, res, next) => { | |
const albumId = req.params.id | |
const albumData = req.body | |
console.log(`Updating album by ID ${albumId}`) | |
updateAlbum(albumId, albumData) | |
.then((data) => res.json({ data })) | |
.catch((error) => next(error)) | |
}) | |
// Delete Album | |
app.delete('/albums/:id', (req, res, next) => { | |
const albumId = req.params.id | |
console.log(`Deleting album by ID ${albumId}`) | |
removeAlbum(albumId) | |
.then((data) => res.status(204).send()) | |
.catch((error) => next(error)) | |
}) | |
// Create Purchase | |
app.post('/purchases', (req, res, next) => { | |
const purchase = req.body | |
console.log('Creating new purchase...') | |
savePurchase(purchase) | |
.then((data) => { | |
return data.populate([ 'album', 'user' ]).execPopulate() | |
}) | |
.then((data) => { | |
res.json({ data }) | |
}) | |
.catch((error) => next(error)) | |
}) | |
// Application Startup | |
app.use(errorHandler) | |
app.listen(3000, () => { | |
console.info('Server listening on port 3000...') | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment