Skip to content

Instantly share code, notes, and snippets.

@GeroSalas
Created May 1, 2018 21:58
Show Gist options
  • Save GeroSalas/74c6a504d377309495c2b9636501dd45 to your computer and use it in GitHub Desktop.
Save GeroSalas/74c6a504d377309495c2b9636501dd45 to your computer and use it in GitHub Desktop.
Simple API REST (NodeJS - Express - MongoDB)
// 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