Skip to content

Commit

Permalink
v2 init
Browse files Browse the repository at this point in the history
  • Loading branch information
hueter committed Nov 13, 2018
0 parents commit 7fb75a3
Show file tree
Hide file tree
Showing 42 changed files with 7,708 additions and 0 deletions.
Empty file added .eslintignore
Empty file.
18 changes: 18 additions & 0 deletions .eslintrc.json
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"
}
}
8 changes: 8 additions & 0 deletions .gitignore
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
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
8.11.3
58 changes: 58 additions & 0 deletions README.md
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.
62 changes: 62 additions & 0 deletions app/app.js
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;
12 changes: 12 additions & 0 deletions app/config.js
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
};
42 changes: 42 additions & 0 deletions app/handlers/error.js
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
};
40 changes: 40 additions & 0 deletions app/handlers/favorites.js
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
};
7 changes: 7 additions & 0 deletions app/handlers/index.js
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');
36 changes: 36 additions & 0 deletions app/handlers/login.js
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;
17 changes: 17 additions & 0 deletions app/handlers/stories.js
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 };
82 changes: 82 additions & 0 deletions app/handlers/story.js
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 };
Loading

0 comments on commit 7fb75a3

Please sign in to comment.