-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 7fb75a3
Showing
42 changed files
with
7,708 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"extends": "eslint:recommended", | ||
"env": { | ||
"es6": true, | ||
"node": true, | ||
"browser": false, | ||
"mocha": true | ||
}, | ||
"parserOptions": { | ||
"ecmaVersion": 2017, | ||
"ecmaFeatures": { | ||
"experimentalObjectRestSpread": true | ||
} | ||
}, | ||
"rules": { | ||
"no-unused-vars": "warn" | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# Dependency directories | ||
node_modules | ||
|
||
# Optional npm cache directory | ||
.npm | ||
.env | ||
|
||
.DS_Store |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
8.11.3 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Hack or Snooze API | ||
|
||
## Live URL | ||
|
||
This API is currently running here: | ||
[https://hack-or-snooze.herokuapp.com/](https://hack-or-snooze.herokuapp.com/) | ||
|
||
## Documentation | ||
|
||
Full interactive API documentation available here: | ||
[https://hackorsnoozeapi.docs.apiary.io](https://hackorsnoozeapi.docs.apiary.io) | ||
|
||
## Development | ||
|
||
### Prerequisites to Running Locally | ||
|
||
1. Install Node | ||
1. Install MongoDB | ||
1. (For Testing) Install Dredd `npm i dredd -g` | ||
|
||
### How to Run Locally | ||
|
||
1. `npm i` | ||
1. In a new tab, `mongod` (unless it's already running, e.g. `brew services | ||
start mongodb`) | ||
1. In the first tab, `npm run dev` | ||
|
||
Server runs on http://localhost:5000 by default. | ||
|
||
You can also pass any environment variables in a `.env` file. | ||
|
||
### How to Run Tests | ||
|
||
[Dredd](http://dredd.org/en/latest/index.html) tests run by reading the | ||
documentation blueprint against a running server. It must be installed globally | ||
to run tests. | ||
|
||
```bash | ||
npm i -g dredd | ||
``` | ||
|
||
After starting the database and server, run the commmand: | ||
|
||
```bash | ||
dredd documentation.apib http://localhost:5000 -f ./tests/api-hooks.js | ||
``` | ||
|
||
### Notes on Data Model | ||
|
||
We have two main entities: **User** and **Story**. | ||
|
||
1 User has many stories. 1 User also has many favorites (which are stories). | ||
|
||
User `stories` and `favorites` exist as arrays of [Mongoose refs which are populated](http://mongoosejs.com/docs/populate.html) in the retrieval methods. | ||
|
||
Each Story document maintains a `username`, which is not a DB reference. This is because Story does not need to know about User in the sense that you would never query a list of stories and have embedded user profiles, whereas the inverse (show stories embedded on User profiles) makes more sense. | ||
|
||
Also, the routing (and consequently, much of the querying) is based on `username`s, which are user-generated, and `storyId`s, which are auto-generated by the server. At no point should the MongoDB `_id` / primary key be exposed to the API users. |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// npm packages | ||
const bodyParser = require('body-parser'); | ||
const dotenv = require('dotenv'); | ||
const express = require('express'); | ||
const mongoose = require('mongoose'); | ||
Promise = require('bluebird'); // eslint-disable-line | ||
|
||
// app imports | ||
const { ENV, MONGODB_URI } = require('./config'); | ||
const { loginHandler, errorHandler, userHandler } = require('./handlers'); | ||
const { storiesRouter, usersRouter } = require('./routers'); | ||
|
||
// global constants | ||
dotenv.config(); | ||
const app = express(); | ||
const { | ||
bodyParserHandler, | ||
globalErrorHandler, | ||
fourOhFourHandler, | ||
fourOhFiveHandler | ||
} = errorHandler; | ||
|
||
// database | ||
mongoose.Promise = Promise; | ||
if (ENV === 'development') { | ||
mongoose.set('debug', true); | ||
} | ||
|
||
mongoose.connect( | ||
MONGODB_URI, | ||
{ autoIndex: true, useNewUrlParser: true } | ||
); | ||
|
||
// body parser setup | ||
app.use(bodyParser.urlencoded({ extended: true })); | ||
app.use(bodyParser.json({ type: '*/*' })); | ||
app.use(bodyParserHandler); // error handling specific to body parser only | ||
|
||
// response headers setup | ||
app.use((request, response, next) => { | ||
response.header('Access-Control-Allow-Origin', '*'); | ||
response.header( | ||
'Access-Control-Allow-Headers', | ||
'Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Authorization' | ||
); | ||
response.header( | ||
'Access-Control-Allow-Methods', | ||
'POST,GET,PATCH,DELETE,OPTIONS' | ||
); | ||
response.header('Content-Type', 'application/json'); | ||
return next(); | ||
}); | ||
|
||
app.post('/signup', userHandler.createUser); | ||
app.post('/login', loginHandler); | ||
app.use('/stories', storiesRouter); | ||
app.use('/users', usersRouter); | ||
app.get('*', fourOhFourHandler); // catch-all for 404 "Not Found" errors | ||
app.all('*', fourOhFiveHandler); // catch-all for 405 "Method Not Allowed" errors | ||
app.use(globalErrorHandler); | ||
|
||
module.exports = app; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
const ENV = process.env.NODE_ENV; | ||
const JWT_SECRET_KEY = process.env.JWT_SECRET_KEY || 'abc123'; | ||
const MONGODB_URI = | ||
process.env.MONGODB_URI || 'mongodb://localhost/hackorsnooze'; | ||
const PORT = process.env.PORT || 5000; | ||
|
||
module.exports = { | ||
ENV, | ||
JWT_SECRET_KEY, | ||
MONGODB_URI, | ||
PORT | ||
}; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
const { APIError } = require('../helpers'); | ||
|
||
function bodyParserHandler(error, request, response, next) { | ||
if (error instanceof SyntaxError || error instanceof TypeError) { | ||
return next(new APIError(400, 'Bad Request', 'Malformed JSON.')); | ||
} | ||
} | ||
|
||
function fourOhFourHandler(request, response, next) { | ||
return next( | ||
new APIError( | ||
404, | ||
'Resource Not Found', | ||
`${request.path} is not valid path to a Hack-Or-Snooze API resource.` | ||
) | ||
); | ||
} | ||
|
||
function fourOhFiveHandler(request, response, next) { | ||
return next( | ||
new APIError( | ||
405, | ||
'Method Not Allowed', | ||
`${request.method} method is not supported at ${request.path}.` | ||
) | ||
); | ||
} | ||
|
||
function globalErrorHandler(error, request, response, next) { | ||
let err = error; | ||
if (!(error instanceof APIError)) { | ||
err = new APIError(500, error.type, error.message); | ||
} | ||
return response.status(err.status).json(err); | ||
} | ||
|
||
module.exports = { | ||
bodyParserHandler, | ||
fourOhFourHandler, | ||
fourOhFiveHandler, | ||
globalErrorHandler | ||
}; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// app imports | ||
const { User, Story } = require('../models'); | ||
const { formatResponse, ensureCorrectUser } = require('../helpers'); | ||
|
||
function addUserFavorite(request, response, next) { | ||
const { username, storyId } = request.params; | ||
const correctUser = ensureCorrectUser( | ||
request.headers.authorization, | ||
username | ||
); | ||
if (correctUser !== 'OK') { | ||
return next(correctUser); | ||
} | ||
return User.readUser(username) | ||
.then(() => Story.getMongoId(storyId)) | ||
.then(story_id => User.addOrDeleteFavorite(username, story_id, 'add')) | ||
.then(user => response.json(formatResponse(user))) | ||
.catch(err => next(err)); | ||
} | ||
|
||
function deleteUserFavorite(request, response, next) { | ||
const { username, storyId } = request.params; | ||
const correctUser = ensureCorrectUser( | ||
request.headers.authorization, | ||
username | ||
); | ||
if (correctUser !== 'OK') { | ||
return next(correctUser); | ||
} | ||
return User.readUser(username) | ||
.then(() => Story.getMongoId(storyId)) | ||
.then(story_id => User.addOrDeleteFavorite(username, story_id, 'delete')) | ||
.then(user => response.json(formatResponse(user))) | ||
.catch(err => next(err)); | ||
} | ||
|
||
module.exports = { | ||
addUserFavorite, | ||
deleteUserFavorite | ||
}; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
exports.loginHandler = require('./login'); | ||
exports.errorHandler = require('./error'); | ||
exports.favoritesHandler = require('./favorites'); | ||
exports.storyHandler = require('./story'); | ||
exports.storiesHandler = require('./stories'); | ||
exports.userHandler = require('./user'); | ||
exports.usersHandler = require('./users'); |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// npm packages | ||
const jwt = require('jsonwebtoken'); | ||
const { validate } = require('jsonschema'); | ||
const bcrypt = require('bcrypt'); | ||
// app imports | ||
const { JWT_SECRET_KEY } = require('../config'); | ||
const { User } = require('../models'); | ||
const { APIError, formatResponse, validateSchema } = require('../helpers'); | ||
const { loginSchema } = require('../schemas'); | ||
|
||
async function auth(request, response, next) { | ||
const validSchema = validateSchema( | ||
validate(request.body, loginSchema), | ||
'user' | ||
); | ||
if (validSchema !== 'OK') { | ||
return next(validSchema); | ||
} | ||
|
||
try { | ||
const user = await User.readUser(request.body.username); | ||
const isValid = bcrypt.compareSync(request.body.password, user.password); | ||
if (!isValid) { | ||
throw new APIError(401, 'Unauthorized', 'Invalid password.'); | ||
} | ||
const userAndToken = { | ||
token: jwt.sign({ username: user.username }, JWT_SECRET_KEY), | ||
...user | ||
}; | ||
return response.json(formatResponse(userAndToken)); | ||
} catch (error) { | ||
return next(error); | ||
} | ||
} | ||
|
||
module.exports = auth; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// app imports | ||
const { Story } = require('../models'); | ||
const { formatResponse, parseSkipLimit } = require('../helpers'); | ||
|
||
function readStories(request, response, next) { | ||
let skip = parseSkipLimit(request.query.skip, null, 'skip') || 0; | ||
let limit = parseSkipLimit(request.query.limit, 25, 'limit') || 25; | ||
if (typeof skip !== 'number') { | ||
return next(skip); | ||
} else if (typeof limit !== 'number') { | ||
return next(limit); | ||
} | ||
return Story.readStories({}, {}, skip, limit) | ||
.then(stories => response.json(formatResponse(stories))) | ||
.catch(err => next(err)); | ||
} | ||
module.exports = { readStories }; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// npm packages | ||
const { validate } = require('jsonschema'); | ||
|
||
// app imports | ||
const { User, Story } = require('../models'); | ||
const { | ||
ensureCorrectUser, | ||
formatResponse, | ||
validateSchema | ||
} = require('../helpers'); | ||
const { storyNewSchema, storyUpdateSchema } = require('../schemas'); | ||
|
||
function createStory(request, response, next) { | ||
const validSchema = validateSchema( | ||
validate(request.body, storyNewSchema), | ||
'story' | ||
); | ||
if (validSchema !== 'OK') { | ||
return next(validSchema); | ||
} | ||
const { username } = request.body.data; | ||
const correctUser = ensureCorrectUser( | ||
request.headers.authorization, | ||
username | ||
); | ||
if (correctUser !== 'OK') { | ||
throw correctUser; | ||
} | ||
return User.readUser(username) | ||
.then(() => Story.createStory(new Story(request.body.data))) | ||
.then(story => response.status(201).json(formatResponse(story))) | ||
.catch(err => next(err)); | ||
} | ||
|
||
function readStory(request, response, next) { | ||
return Story.readStory(request.params.storyId) | ||
.then(story => response.json(formatResponse(story))) | ||
.catch(err => next(err)); | ||
} | ||
|
||
function updateStory(request, response, next) { | ||
const { storyId } = request.params; | ||
const validSchema = validateSchema( | ||
validate(request.body, storyUpdateSchema), | ||
'story' | ||
); | ||
if (validSchema !== 'OK') { | ||
return next(validSchema); | ||
} | ||
return Story.readStory(storyId) | ||
.then(story => { | ||
const correctUser = ensureCorrectUser( | ||
request.headers.authorization, | ||
story.username | ||
); | ||
if (correctUser !== 'OK') { | ||
throw correctUser; | ||
} | ||
}) | ||
.then(() => Story.updateStory(storyId, request.body.data)) | ||
.then(story => response.json(formatResponse(story))) | ||
.catch(err => next(err)); | ||
} | ||
|
||
function deleteStory(request, response, next) { | ||
const { storyId } = request.params; | ||
return Story.readStory(storyId) | ||
.then(story => { | ||
const correctUser = ensureCorrectUser( | ||
request.headers.authorization, | ||
story.username | ||
); | ||
if (correctUser !== 'OK') { | ||
throw correctUser; | ||
} | ||
}) | ||
.then(() => Story.deleteStory(storyId)) | ||
.then(story => response.json(formatResponse(story))) | ||
.catch(err => next(err)); | ||
} | ||
|
||
module.exports = { createStory, readStory, updateStory, deleteStory }; |
Oops, something went wrong.