Skip to content

Restructure, clean house, use docker, use dotenv... #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
821334f
Fix code-smells like redudant comments, lack of spacing, missing var/…
rm-rf-etc Jul 14, 2018
e3cb1fc
Prevent parameter injection
rm-rf-etc Jul 14, 2018
3d943be
-
rm-rf-etc Jul 14, 2018
a3b7ffb
Remove unnecessary IIFE, use more destructuring
rm-rf-etc Jul 14, 2018
5c04672
add docker launch script
rm-rf-etc Jul 16, 2018
c32e13d
wip
rm-rf-etc Jul 17, 2018
300e8d4
Restructure for dockerizing
rm-rf-etc Jul 17, 2018
9d9bd30
Make API & DB launch together with `docker-compose up`.
rm-rf-etc Jul 18, 2018
8de739a
Fix structure and stuff.
rm-rf-etc Jul 18, 2018
065235a
Fix example-env contents
rm-rf-etc Jul 18, 2018
337e6db
Update README.md
rm-rf-etc Jul 18, 2018
a9383b1
Update README.md
rm-rf-etc Jul 18, 2018
7a58cd7
Update README.md
rm-rf-etc Jul 18, 2018
481a3e4
wip 1
rm-rf-etc Jul 19, 2018
c67210f
Make env vars work on local, in docker build, and docker-compose.
rm-rf-etc Jul 19, 2018
edd9a2e
Fix start commands, update readme, other misc too.
rm-rf-etc Jul 19, 2018
f7035d5
Update README.md
rm-rf-etc Jul 19, 2018
6a80439
Separate .env vars from hostname vars, improve bootstrap.js dependabi…
rm-rf-etc Jul 20, 2018
6c2d9c1
Update readme, remove unused env var.
rm-rf-etc Jul 20, 2018
bee0350
Rewrite & make auth controllers "thin", make JWT work, try to improve…
rm-rf-etc Jul 20, 2018
06a82b8
Fix needed updates to ENV vars and readme.
rm-rf-etc Jul 20, 2018
3d96cfb
Move controllers to individual files.
rm-rf-etc Jul 20, 2018
7f4eeb4
Add node version to readme
rm-rf-etc Jul 21, 2018
6bfee01
Remove expose in dockerfile
rm-rf-etc Jul 21, 2018
66c7cd4
Update dockerfile to expose port
rm-rf-etc Jul 23, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DB_PORT_EXTERNAL=8529
API_PORT_EXTERNAL=9191
HOSTNAME=your hostname goes here
COMPOSE_PROJECT_NAME=platform
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
node_modules/
node_modules/
env/*
!env/your hostname here
.env
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM node:9
EXPOSE 9191
WORKDIR /home/node
COPY package.json /home/node
COPY yarn.lock /home/node
COPY app /home/node/app
RUN yarn install --pure-lockfile
CMD npm run start:containerized
86 changes: 68 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,103 @@
# node-rest-auth-arangodb

Creating API with JWT user authentication using the following:
An API with JWT user authentication. Project works out of the box with Docker, or just localhost.
Be sure to follow the setup instructions so that env vars are passed in.

* nodejs
* Express
* jwt
* passport
* ArangoDB
Made using:
* [Nodejs](https://nodejs.org)
* [Express](https://expressjs.com)
* [jwt-simple](https://www.npmjs.com/package/jwt-simple)
* [Passport](http://www.passportjs.org)
* [ArangoDB](https://www.arangodb.com)

_Only tested with macOS_


## Requirements

0. Install [ArangoDB](https://www.arangodb.com/)
1. Install [Node.js](https://nodejs.org/en/)
* [Docker](https://www.docker.com/get-docker)

### Optional Requirements

* [Node.js](https://nodejs.org/en/) version 9+


## Setup

* Update `HOSTNAME=your hostname goes here` in file `.env-example`
* Rename `.env-example` to `.env`
* Rename `env/your hostname here` to match your hostname exactly

You can get the hostname of your machine by running `hostname` in the command terminal.


## To Run

* `docker-compose up -d` (or just `docker-compose up`)

## Configure `/include/config.js`
When done, `docker-compose stop` or `docker-compose down`. I notice the API image has to be
erased manually to get the code to update, use `docker image remove <image ID>`.

0. Set JWT secret key
1. Set location to ArangoDB
2. Set database in ArangoDB

## Install
## To Run On localhost

`npm install`
If you'd rather run node on localhost, then...

## Run
* `yarn install`
* (Yarn is faster than npm, if you don't have it, do `npm install -g yarn`)
* Rename `env/your hostname here` to match your hostname exactly
* (If you don't know your hostname, run `hostname` in the terminal)
* Change the `ARANGODB_DOCKER_NAME` ENV var to `localhost` instead of `database`
* `docker-compose up -d database` to run ArangoDB (or just `docker-compose up database`)
* `npm start` to run the API on localhost, or `npm run start:watch` for auto-reload

0. Make sure ArangoDB is running
1. `node server`
If you're going to run ArangoDB on localhost, see `scripts/boostrap.js`, this runs
automatically when using `docker-compose`.


## When Done

`docker-compose down` will clean up for you.

`docker-compose build api` (or just `docker-compose build`) will build the API image
(so you can deploy to Kubernetes or whatnot).


## RESTful API endpoints

[Postman](https://www.getpostman.com/) is a great tool for using the API. Otherwise, see the
sample cURL command below.


### POST `/api/users/register`

Register by providing `username` and `password`.
```
curl -X POST http://localhost:9191/api/users/register -d 'username=jim&password=password'
```


### POST `/api/users/authenticate`

Authenticate by providing `username` and `password`.
```
curl -X POST http://localhost:9191/api/users/authenticate -d 'username=jim&password=password'
```


### GET `/api/users/memberinfo`

Get user data by providing `token`.
```
curl -X GET http://localhost:9191/api/users/memberinfo -H 'Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImppbSIsImV4cGlyYXRpb24iOjE1MzIyNzMxMjQwODV9.uKTvxRwZbRMF_WYA5EmVKGvGUJc3Wx9TTjWR7I7MLy4'
```


## Credits

Based on https://github.com/clemudensi/nodeJS_backend


## License

MIT
MIT
37 changes: 37 additions & 0 deletions app/controllers/authenticate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

const {
exitIfMissing,
findUser,
passwordMatch,
jwtEncode,
jsonOkay,
fail,
} = require('./helpers')


module.exports = async (req, res)=> {
try {

const { username, password } = req.body
if (exitIfMissing(res, username, password)) {
return
}

const user = await findUser(username)
if (!user) {
return fail(res, 'Authentication failed, no match')
}

if (passwordMatch(user, password)) {
const { payload, token } = jwtEncode(req, username)
return jsonOkay(res, { token, payload })
}
else {
return fail(res, 'Invalid credentials')
}

} catch (err) {
fail(res, err.message)
console.log(err)
}
}
90 changes: 90 additions & 0 deletions app/controllers/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use strict'

// DB_PORT_INTERNAL = 8529

const {
JWT_SECRET,
ARANGODB_DOCKER_NAME,
DB_USERNAME,
DB_USERPASS,
DB_NAME,
} = process.env

const jwtSimple = require('jwt-simple') // <-- I suspect this is a hack
const bcrypt = require('bcryptjs')

const { Database, aql } = require('arangojs')
const db = new Database(`http://${ARANGODB_DOCKER_NAME}:8529`)
db.useDatabase(DB_NAME)
db.useBasicAuth(DB_USERNAME, DB_USERPASS)

const salt = bcrypt.genSaltSync(10)
const second = 1000
const minute = 60 * second
const hour = 60 * minute
const day = 24 * hour


async function userRegister(username, password) {

const hash = bcrypt.hashSync(password, salt)

return await db.collection('users').save({ username, password: hash })
}

async function findUser(username) {

const cursor = await db.query(aql`FOR user IN users
FILTER user.username==${String(username)}
RETURN user`)

return await cursor.next()
}

const passwordMatch = (user, plaintext)=> bcrypt.compareSync(plaintext, user.password)

const okay = (res, message)=> jsonMessage(res, true, message)

const fail = (res, message)=> jsonMessage(res, false, message)

const jsonOkay = (res, json)=> res.json({ success: true, ...json })

const jsonMessage = (res, status, message)=> res.json({ success: status, msg: message })

const jwtDecode = headers=>
headers && headers.authorization
? jwtSimple.decode(headers.authorization.replace(/^JWT\s/,''), JWT_SECRET)
: null

const jwtEncode = (req, username)=> {

// using data from req is not yet implemented
const payload = {
username,
expiration: Date.now() + 2 * day,
}

return { payload, token: `JWT ${jwtSimple.encode(payload, JWT_SECRET)}` }
}

function exitIfMissing(res, username, password) {

if (!username || !password) {

fail(res, `Missing ${
andify([[!username, 'username'], [!password, 'password']])
}`)

return true
}
return false
}

function andify(words) {
const missing = []
words.forEach(word=> word[0] ? missing.push(word[1]) : 0)

return missing.join(' and ')
}

module.exports = { andify, exitIfMissing, jwtEncode, jwtDecode, jsonOkay, fail, okay, findUser, userRegister, passwordMatch }
26 changes: 26 additions & 0 deletions app/controllers/memberinfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

const {
fail,
findUser,
jsonOkay,
} = require('./helpers')


module.exports = async (req, res, done)=> {
try {

const { jwt } = req
if (!jwt || !jwt.username) {
return fail(res, 'Unknown auth error')
}

const user = await findUser(jwt.username)
return user
? jsonOkay(res, { msg:'Welcome', data:user })
: fail(res, 'Authentication failed. User not found')

} catch (err) {
fail(res, err.message)
console.log(err)
}
}
31 changes: 31 additions & 0 deletions app/controllers/register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

const {
exitIfMissing,
findUser,
userRegister,
okay,
fail,
} = require('./helpers')


module.exports = async (req, res)=> {
try {

const { username, password } = req.body
if (exitIfMissing(res, username, password)) {
return
}

if (await findUser(username)) {
return fail(res, 'That username is already taken')
}

const regResult = await userRegister(username, password)
console.log('regResult', regResult)
return okay(res, 'Registration successful')

} catch (err) {
fail(res, err.message)
console.log(err)
}
}
61 changes: 61 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict'

// API_PORT_INTERNAL = 9191

const { ENABLE_HTTPS } = process.env

const fs = require('fs')
const https = require('https')
const http = require('http')
const helmet = require('helmet')
const bodyParser = require('body-parser')
const morgan = require('morgan')
const authRoutes = require('./routes')
const express = require('express')
const app = express()

app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

app.use(morgan('dev'))

// use security middleware
app.use(helmet())

app.get('/', (req, res)=> {
res.send(`Hello from API`)
})

app.all('/api/*', (req, res, next)=> {
res.set('Access-Control-Allow-Origin', '*')
res.set('Access-Control-Allow-Methods', 'GET, POST, DELETE, PUT')
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method == 'OPTIONS') return res.sendStatus(200)
next()
})

app.get('/api/', (req, res)=> {
res.send(`Hello from API`)
})

app.use('/api/users', authRoutes)


if (JSON.parse(ENABLE_HTTPS)) {
const ssl = {
key: fs.readFileSync('./certs/1.key'),
cert: fs.readFileSync('./certs/root-ca.crt'),
ca: fs.readFileSync('./certs/intermediate.crt'),
}
https.createServer(ssl, app).listen(9191, ()=> {
console.log('Using HTTPS')
console.log(`API is now running on port 9191`)
})
}
else {
app.listen(9191, ()=> {
console.log('Using HTTP')
console.log(`API is now running on port 9191`)
})
}

Loading