Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
17 changes: 17 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# slugchat

## configuration
configure a .env file to include the following

``` bash
PORT=3000
NODE_ENV='dev'
SECRET='shark in the dark'
API_URL='http://localhost:3000'
CLIENT_URL='http://localhost:8080'
CORS_ORIGINS='http://localhost:8080'
MONGODB_URI='mongodb://localhost/slugchat-dev'
GOOGLE_CLIENT_SECRET='<put google client secret here>'
GOOGLE_CLIENT_ID='<put your google cleint id here>'
```

8 changes: 8 additions & 0 deletions backend/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict'
// load env
require('dotenv').config()
// assert env
require('./src/lib/assert-env.js')
// start server
require('babel-register')
require('./src/main.js')
40 changes: 40 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "slugchat",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"lint": "eslint .",
"start": "node index.js",
"watch": "nodemon index.js",
"test": "jest --runInBand",
"test-watch": "jest --watch --runInBand",
"mongo-on": "mkdir -p ./db && mongod --dbpath ./db",
"mongo-off": "killall mongod"
},
"dependencies": {
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"babel-register": "^6.24.1",
"bcrypt": "^1.0.2",
"body-parser": "^1.17.2",
"cors": "^2.8.4",
"dotenv": "^4.0.0",
"express": "^4.15.3",
"http-errors": "^1.6.2",
"jsonwebtoken": "^7.4.2",
"lodash": "^4.17.4",
"mongoose": "^4.11.5",
"morgan": "^1.8.2",
"socket.io": "^2.0.3",
"superagent": "^3.5.2"
},
"devDependencies": {
"faker": "^4.1.0",
"jest": "^20.0.4",
"nodemon": "^1.11.0"
},
"description": "## configuration configure a .env file to include the following",
"keywords": [],
"author": ""
}
4 changes: 4 additions & 0 deletions backend/src/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": ["es2015"],
"plugins": ["transform-object-rest-spread"]
}
23 changes: 23 additions & 0 deletions backend/src/lib/assert-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
let required = [
'PORT',
'SECRET',
'API_URL',
'NODE_ENV',
'CLIENT_URL',
'MONGODB_URI',
'CORS_ORIGINS',
'GOOGLE_CLIENT_ID',
'GOOGLE_CLIENT_SECRET',
]

try {
required.forEach(key => {
if(!process.env[key])
throw new Error(`ENVIRONMNET ERROR: slugchat requires process.env.${key} to be set`)
})
} catch (e) {
console.error(e.message)
process.exit(1)
}


35 changes: 35 additions & 0 deletions backend/src/lib/mongo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict'

// DEPENDENCIES
const mongoose = require('mongoose')
mongoose.Promise = Promise

// STATE
const state = {
isOn: false,
config: {
useMongoClient: true,
promiseLibrary: Promise,
},
}

// INTERFACE
export const start = () => {
if(state.isOn)
return Promise.reject(new Error('USER ERROR: db is connected'))
return mongoose.connect(process.env.MONGODB_URI, state.config)
.then(() => {
console.log('__MONGO_CONNECTED__', process.env.MONGODB_URI)
state.isOn = true
})
}

export const stop = () => {
if(!state.isOn)
return Promise.reject(new Error('USER ERROR: db is disconnected'))
return mongoose.disconnect()
.then(() => {
state.isOn = false
console.log('__MONGO_DISCONNECTED__')
})
}
9 changes: 9 additions & 0 deletions backend/src/lib/promisify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default (fn) => (...args) => {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if(err)
return reject(err)
resolve(data)
})
})
}
67 changes: 67 additions & 0 deletions backend/src/lib/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use strict'

// DEPENDENCIES
import cors from 'cors'
import morgan from 'morgan'
import express from 'express'
import * as mongo from './mongo.js'

import authRouter from '../router/auth.js'
import fourOhFour from '../middleware/four-oh-four.js'
import errorHandler from '../middleware/error-middleware.js'

// STATE
const app = express()

// global middleware
app.use(morgan('dev'))
app.use(cors({
origin: process.env.CORS_ORIGINS.split(' '),
credentials: true,
}))

// routers
app.use(authRouter)

// handle errors
app.use(fourOhFour)
app.use(errorHandler)

const state = {
isOn: false,
http: null,
}

// INTERFACE
export const start = () => {
return new Promise((resolve, reject) => {
if (state.isOn)
return reject(new Error('USAGE ERROR: the state is on'))
state.isOn = true
mongo.start()
.then(() => {
state.http = app.listen(process.env.PORT, () => {
console.log('__SERVER_UP__', process.env.PORT)
resolve()
})
})
.catch(reject)
})
}

export const stop = () => {
return new Promise((resolve, reject) => {
if(!state.isOn)
return reject(new Error('USAGE ERROR: the state is off'))
return mongo.stop()
.then(() => {
state.http.close(() => {
console.log('__SERVER_DOWN__')
state.isOn = false
state.http = null
resolve()
})
})
.catch(reject)
})
}
3 changes: 3 additions & 0 deletions backend/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as server from './lib/server.js'

server.start()
29 changes: 29 additions & 0 deletions backend/src/middleware/basic-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import User from '../model/user.js'
import createError from 'http-errors'

export default (req, res, next) => {
let {authorization} = req.headers
if(!authorization)
return next(createError(400, 'AUTH ERROR: no authorization header'))

let encoded = authorization.split('Basic ')[1]
if(!encoded)
return next(createError(400, 'AUTH ERROR: not basic auth'))

let decoded = new Buffer(encoded, 'base64').toString()
let [username, password] = decoded.split(':')
if(!username || !password)
return next(createError(401, 'AUTH ERROR: username or password missing'))

User.findOne({username})
.then(user => {
if(!user)
throw createError(401, 'AUTH ERROR: user not found')
return user.passwordCompare(password)
})
.then(user => {
req.user = user
next()
})
.catch(next)
}
24 changes: 24 additions & 0 deletions backend/src/middleware/bearer-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as jwt from 'jsonwebtoken'
import User from '../model/user.js'
import createError from 'http-errors'
import promisify from '../lib/promisify.js'

export default (req, res, next) => {
let {authorization} = req.headers
if(!authorization)
return next(createError(400, 'AUTH ERROR: no authorization header'))

let token = authorization.split('Bearer ')[1]
if(!token)
return next(createError(400, 'AUTH ERROR: not bearer auth'))

promisify(jwt.verify)(token, process.env.SECRET)
.then(({randomHash}) => User.findOne({randomHash}))
.then((user) => {
if(!user)
throw createError(401, 'AUTH ERROR: user not found')
req.user = user
next()
})
.catch(err => createError(401, err))
}
22 changes: 22 additions & 0 deletions backend/src/middleware/error-middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// INTERFACE
export default (err, req, res, next) => {
console.error(err)
if(err.status)
return res.sendStatus(err.status)

err.message = err.message.toLowerCase()

if(err.message.includes('validation failed'))
return res.sendStatus(400)

if(err.message.includes('duplicate key'))
return res.sendStatus(409)

if(err.message.includes('objectid failed'))
return res.sendStatus(404)

if(err.message.includes('unauthorized'))
return res.sendStatus(401)

res.sendStatus(500)
}
6 changes: 6 additions & 0 deletions backend/src/middleware/four-oh-four.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// DEPENDENCIES
import createError from 'http-errors'

// INTERFACE
export default (req, res, next) =>
next(createError(404, `USER ERROR: ${req.url.path} not a route`))
60 changes: 60 additions & 0 deletions backend/src/model/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict'

// DEPENDECIES
import * as bcrypt from 'bcrypt'
import {randomBytes} from 'crypto'
import * as jwt from 'jsonwebtoken'
import createError from 'http-errors'
import {promisify} from '../lib/promisify.js'
import Mongoose, {Schema} from 'mongoose'

// SCHEMA
const userSchema = new Schema({
email: {type: String, required: true, unique: true},
username: {type: String, required: true, unique: true},
passwordHash: {type: String, required: true},
tokenSeed: {type: String, unique: true, default: ''},
})

// INSTANCE METHODS
userSchema.methods.passwordCompare = function(password){
return bcrypt.compare(password, this.passwordHash)
.then(success => {
if (!success)
throw createError(401, 'AUTH ERROR: wrong password')
return this
})
}

userSchema.methods.tokenCreate = function(){
this.tokenSeed = randomBytes(32).toString('base64')
return this.save()
.then(user => {
return jwt.sign({tokenSeed: this.tokenSeed}, process.env.SECRET)
})
.then(token => {
return token
})
}

// MODEL
const User = Mongoose.model('user', userSchema)

// STATIC METHODS
User.createFromSignup = function (user) {
if(!user.password || !user.email || !user.username)
return Promise.reject(
createError(400, 'VALIDATION ERROR: missing username email or password '))

let {password} = user
user = Object.assign({}, user, {password: undefined})

return bcrypt.hash(password, 1)
.then(passwordHash => {
let data = Object.assign({}, user, {passwordHash})
return new User(data).save()
})
}

// INTERFACE
export default User
34 changes: 34 additions & 0 deletions backend/src/router/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict'

import {Router} from 'express'
import User from '../model/user.js'
import bodyParser from 'body-parser'
import basicAuth from '../middleware/basic-auth.js'

export default new Router()
.post('/signup', bodyParser.json() , (req, res, next) => {
new User.createFromSignup(req.body)
.then(user => user.tokenCreate())
.then(token => {
res.cookie('X-Slugchat-Token', token)
res.send(token)
})
.catch(next)
})
.get('/usernames/:username', (req, res, next) => {
User.findOne({username: username})
.then(user => {
if(!user)
return res.sendStatus(409)
return res.sendStatus(200)
})
.catch(next)
})
.get('/login', basicAuth, (req, res, next) => {
req.user.tokenCreate()
.then((token) => {
res.cookie('X-Slugchat-Token', token)
res.send(token)
})
.catch(next)
})
Loading