diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..0979a6c0 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/node_modules/** diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..8a17f9d5 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,18 @@ +{ + "extends": [ + "kentcdodds", + "kentcdodds/jest" + ], + "rules": { + "max-len": [ + 2, + 80 + ], + "import/newline-after-import": "off", + "import/no-unassigned-import": "off", + "no-console": "off", + "func-names": "off", + "complexity": ["error", 8], + "babel/new-cap": "off", + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f0929ee7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +node_modules +coverage +build +.DS_Store +.env +*.log +npm-debug.log* +.mongo-db diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000..3a982589 --- /dev/null +++ b/api/README.md @@ -0,0 +1,37 @@ +# ![Node/Express/Mongoose Example App](project-logo.png) + +> Example Node (Express+Mongoose) codebase that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) API spec. + +This repo is functionality complete but still in beta while we resolve bugs, etc -- PR's and issues welcome! + +# Code Overview + +## Dependencies + +- [expressjs](https://github.com/expressjs/express) - The server for handling and routing HTTP requests +- [express-jwt](https://github.com/auth0/express-jwt) - Middleware for validating JWTs for authentication +- [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) - For generating JWTs used by authentication +- [mongoose](https://github.com/Automattic/mongoose) - For modeling and mapping MongoDB data to javascript +- [mongoose-unique-validator](https://github.com/blakehaswell/mongoose-unique-validator) - For handling unique validation errors in Mongoose. Mongoose only handles validation at the document level, so a unique index across a collection will throw an excpetion at the driver level. The `mongoose-unique-validator` plugin helps us by formatting the error like a normal mongoose `ValidationError`. +- [passport](https://github.com/jaredhanson/passport) - For handling user authentication +- [slug](https://github.com/dodo/node-slug) - For encoding titles into a URL-friendly format + +## Application Structure + +- `app.js` - The entry point to our application. This file defines our express server and connects it to MongoDB using mongoose. It also requires the routes and models we'll be using in the application. +- `config/` - This folder contains configuration for passport as well as a central location for configuration/environment variables. +- `routes/` - This folder contains the route definitions for our API. They contain +- `models/` - This folder contains the schema definitions for our Mongoose models. + +## Error Handling + +In `routes/api/index.js`, we define a error-handling middleware for handling Mongoose's `ValidationError`. This middleware will respond with a 422 status code and format the response to have [error messages the clients can understand](https://github.com/gothinkster/realworld/blob/master/API.md#errors-and-status-codes) + +## Authentication + +Requests are authenticated using the `Authorization` header with a valid JWT. We define two express middlewares in `routes/auth.js` that can be used to authenticate requests. The `required` middleware configures the `express-jwt` middleware using our application's secret and will return a 401 status code if the request cannot be authenticated. The payload of the JWT can then be accessed from `req.payload` in the endpoint. The `optional` middleware configures the `express-jwt` in the same way as `required`, but will *not* return a 401 status code if the request cannot be authenticated. + + +
+ +[![Brought to you by Thinkster](https://raw.githubusercontent.com/gothinkster/realworld/master/media/end.png)](https://thinkster.io) diff --git a/api/app.js b/api/app.js new file mode 100644 index 00000000..a84b35d3 --- /dev/null +++ b/api/app.js @@ -0,0 +1,91 @@ +const express = require('express') +const bodyParser = require('body-parser') +const session = require('express-session') +const cors = require('cors') +const errorhandler = require('errorhandler') +const mongoose = require('mongoose') + +const isProduction = process.env.NODE_ENV === 'production' + +// Create global app object +const app = express() + +app.use(cors()) + +// Normal express config defaults +app.use(require('morgan')('dev')) +app.use(bodyParser.urlencoded({extended: false})) +app.use(bodyParser.json()) + +app.use(require('method-override')()) +app.use(express.static(`${__dirname}/public`)) + +app.use( + session({ + secret: 'conduit', + cookie: {maxAge: 60000}, + resave: false, + saveUninitialized: false, + }), +) + +if (!isProduction) { + app.use(errorhandler()) +} + +if (isProduction) { + mongoose.connect(process.env.MONGODB_URI) +} else { + mongoose.connect('mongodb://localhost/conduit') + mongoose.set('debug', true) +} + +require('./models/User') +require('./models/Article') +require('./models/Comment') +require('./config/passport') + +app.use(require('./routes')) + +/// catch 404 and forward to error handler +app.use((req, res, next) => { + const err = new Error('Not Found') + err.status = 404 + next(err) +}) + +/// error handlers + +// development error handler +// will print stacktrace +if (!isProduction) { + app.use((err, req, res) => { + console.log(err.stack) + + res.status(err.status || 500) + + res.json({ + errors: { + message: err.message, + error: err, + }, + }) + }) +} + +// production error handler +// no stacktraces leaked to user +app.use((err, req, res) => { + res.status(err.status || 500) + res.json({ + errors: { + message: err.message, + error: {}, + }, + }) +}) + +// finally, let's start our server... +const server = app.listen(process.env.PORT || 3000, () => { + console.log(`Listening on port ${server.address().port}`) +}) diff --git a/api/config/index.js b/api/config/index.js new file mode 100644 index 00000000..3cedf6ab --- /dev/null +++ b/api/config/index.js @@ -0,0 +1,3 @@ +module.exports = { + secret: process.env.NODE_ENV === 'production' ? process.env.SECRET : 'secret', +} diff --git a/api/config/passport.js b/api/config/passport.js new file mode 100644 index 00000000..669cac12 --- /dev/null +++ b/api/config/passport.js @@ -0,0 +1,19 @@ +const passport = require('passport') +const LocalStrategy = require('passport-local').Strategy +const mongoose = require('mongoose') +const User = mongoose.model('User') + +passport.use(new LocalStrategy({ + usernameField: 'user[email]', + passwordField: 'user[password]', +}, (email, password, done) => { + User.findOne({email}) + .then(user => { + if (!user || !user.validPassword(password)) { + return done(null, false, {errors: {'email or password': 'is invalid'}}) + } + + return done(null, user) + }) + .catch(done) +})) diff --git a/api/models/Article.js b/api/models/Article.js new file mode 100644 index 00000000..4caa1bef --- /dev/null +++ b/api/models/Article.js @@ -0,0 +1,57 @@ +const mongoose = require('mongoose') +const uniqueValidator = require('mongoose-unique-validator') +const slug = require('slug') +const User = mongoose.model('User') + +const ArticleSchema = new mongoose.Schema( + { + slug: {type: String, lowercase: true, unique: true}, + title: String, + description: String, + body: String, + favoritesCount: {type: Number, default: 0}, + comments: [{type: mongoose.Schema.Types.ObjectId, ref: 'Comment'}], + tagList: [{type: String}], + author: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}, + }, + {timestamps: true}, +) + +ArticleSchema.plugin(uniqueValidator, {message: 'is already taken'}) + +ArticleSchema.pre('validate', function(next) { + this.slugify() // eslint-disable-line babel/no-invalid-this + + next() +}) + +ArticleSchema.methods.slugify = function() { + this.slug = slug(this.title) +} + +ArticleSchema.methods.updateFavoriteCount = function() { + const article = this + + return User.count({favorites: {$in: [article._id]}}).then(count => { + article.favoritesCount = count + + return article.save() + }) +} + +ArticleSchema.methods.toJSONFor = function(user) { + return { + slug: this.slug, + title: this.title, + description: this.description, + body: this.body, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + tagList: this.tagList, + favorited: user ? user.isFavorite(this._id) : false, + favoritesCount: this.favoritesCount, + author: this.author.toProfileJSONFor(user), + } +} + +mongoose.model('Article', ArticleSchema) diff --git a/api/models/Comment.js b/api/models/Comment.js new file mode 100644 index 00000000..a42b0007 --- /dev/null +++ b/api/models/Comment.js @@ -0,0 +1,22 @@ +const mongoose = require('mongoose') + +const CommentSchema = new mongoose.Schema( + { + body: String, + author: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}, + article: {type: mongoose.Schema.Types.ObjectId, ref: 'Article'}, + }, + {timestamps: true}, +) + +// Requires population of author +CommentSchema.methods.toJSONFor = function(user) { + return { + id: this._id, + body: this.body, + createdAt: this.createdAt, + author: this.author.toProfileJSONFor(user), + } +} + +mongoose.model('Comment', CommentSchema) diff --git a/api/models/User.js b/api/models/User.js new file mode 100644 index 00000000..93d22e9e --- /dev/null +++ b/api/models/User.js @@ -0,0 +1,123 @@ +const crypto = require('crypto') +const mongoose = require('mongoose') +const uniqueValidator = require('mongoose-unique-validator') +const jwt = require('jsonwebtoken') +const secret = require('../config').secret + +const UserSchema = new mongoose.Schema( + { + username: { + type: String, + lowercase: true, + unique: true, + required: [true, "can't be blank"], + match: [/^[a-zA-Z0-9]+$/, 'is invalid'], + index: true, + }, + email: { + type: String, + lowercase: true, + unique: true, + required: [true, "can't be blank"], + match: [/\S+@\S+\.\S+/, 'is invalid'], + index: true, + }, + bio: String, + image: String, + favorites: [{type: mongoose.Schema.Types.ObjectId, ref: 'Article'}], + following: [{type: mongoose.Schema.Types.ObjectId, ref: 'User'}], + hash: String, + salt: String, + }, + {timestamps: true}, +) + +UserSchema.plugin(uniqueValidator, {message: 'is already taken.'}) + +UserSchema.methods.validPassword = function(password) { + const hash = crypto + .pbkdf2Sync(password, this.salt, 10000, 512, 'sha512') + .toString('hex') + return this.hash === hash +} + +UserSchema.methods.setPassword = function(password) { + this.salt = crypto.randomBytes(16).toString('hex') + this.hash = crypto + .pbkdf2Sync(password, this.salt, 10000, 512, 'sha512') + .toString('hex') +} + +UserSchema.methods.generateJWT = function() { + const today = new Date() + const exp = new Date(today) + exp.setDate(today.getDate() + 60) + + return jwt.sign( + { + id: this._id, + username: this.username, + exp: parseInt(exp.getTime() / 1000, 10), + }, + secret, + ) +} + +UserSchema.methods.toAuthJSON = function() { + return { + username: this.username, + email: this.email, + token: this.generateJWT(), + } +} + +UserSchema.methods.toProfileJSONFor = function(user) { + return { + username: this.username, + bio: this.bio, + image: ( + this.image || 'https://static.productionready.io/images/smiley-cyrus.jpg' + ), + following: user ? user.isFollowing(this._id) : false, + } +} + +UserSchema.methods.favorite = function(id) { + if (this.favorites.indexOf(id) === -1) { + this.favorites.push(id) + } + + return this.save() +} + +UserSchema.methods.unfavorite = function(id) { + this.favorites.remove(id) + return this.save() +} + +UserSchema.methods.isFavorite = function(id) { + return this.favorites.some(favoriteId => { + return favoriteId.toString() === id.toString() + }) +} + +UserSchema.methods.follow = function(id) { + if (this.favorites.indexOf(id) === -1) { + this.following.push(id) + } + + return this.save() +} + +UserSchema.methods.unfollow = function(id) { + this.following.remove(id) + return this.save() +} + +UserSchema.methods.isFollowing = function(id) { + return this.following.some(followId => { + return followId.toString() === id.toString() + }) +} + +mongoose.model('User', UserSchema) diff --git a/api/package.json b/api/package.json new file mode 100644 index 00000000..2d54b90b --- /dev/null +++ b/api/package.json @@ -0,0 +1,35 @@ +{ + "name": "conduit-node", + "version": "1.0.0", + "description": "conduit on node", + "main": "app.js", + "scripts": { + "start": "node ./app.js", + "dev": "nodemon ./app.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "ISC", + "repository": "git@github.com:kentcdodds/testing-workshop.git", + "dependencies": { + "body-parser": "1.15.0", + "cors": "2.7.1", + "dotenv": "2.0.0", + "ejs": "2.4.1", + "errorhandler": "1.4.3", + "express": "4.13.4", + "express-jwt": "3.3.0", + "express-session": "1.13.0", + "jsonwebtoken": "7.1.9", + "method-override": "2.3.5", + "methods": "1.1.2", + "mongoose": "4.4.10", + "mongoose-unique-validator": "1.0.2", + "morgan": "1.7.0", + "passport": "0.3.2", + "passport-local": "1.0.0", + "request": "2.69.0", + "slug": "0.9.1", + "underscore": "1.8.3" + }, + "author": "Thinkster (https://github.com/gothinkster)" +} diff --git a/api/project-logo.png b/api/project-logo.png new file mode 100644 index 00000000..ef92a4be Binary files /dev/null and b/api/project-logo.png differ diff --git a/api/public/.keep b/api/public/.keep new file mode 100644 index 00000000..e69de29b diff --git a/api/routes/api/articles.js b/api/routes/api/articles.js new file mode 100644 index 00000000..ddd0d234 --- /dev/null +++ b/api/routes/api/articles.js @@ -0,0 +1,309 @@ +const router = require('express').Router() +const mongoose = require('mongoose') +const Article = mongoose.model('Article') +const Comment = mongoose.model('Comment') +const User = mongoose.model('User') +const auth = require('../auth') + +// Preload article objects on routes with ':article' +router.param('article', (req, res, next, slug) => { + Article.findOne({slug}) + .populate('author') + .then(article => { + if (!article) { + return res.sendStatus(404) + } + + req.article = article + + return next() + }) + .catch(next) +}) + +router.param('comment', (req, res, next, id) => { + Comment.findById(id) + .then(comment => { + if (!comment) { + return res.sendStatus(404) + } + + req.comment = comment + + return next() + }) + .catch(next) +}) + +router.get('/', auth.optional, (req, res, next) => { + const query = {} + let limit = 20 + let offset = 0 + + if (typeof req.query.limit !== 'undefined') { + limit = req.query.limit + } + + if (typeof req.query.offset !== 'undefined') { + offset = req.query.offset + } + + if (typeof req.query.tag !== 'undefined') { + query.tagList = {$in: [req.query.tag]} + } + + Promise.all([ + req.query.author ? User.findOne({username: req.query.author}) : null, + req.query.favorited ? User.findOne({username: req.query.favorited}) : null, + ]) + .then(results => { + const author = results[0] + const favoriter = results[1] + + if (author) { + query.author = author._id + } + + if (favoriter) { + query._id = {$in: favoriter.favorites} + } else if (req.query.favorited) { + query._id = {$in: []} + } + + return Promise.all([ + Article.find(query) + .limit(Number(limit)) + .skip(Number(offset)) + .sort({createdAt: 'desc'}) + .populate('author') + .exec(), + Article.count(query).exec(), + req.payload ? User.findById(req.payload.id) : null, + ]).then(([articles, articlesCount, user]) => { + return res.json({ + articles: articles.map(article => { + return article.toJSONFor(user) + }), + articlesCount, + }) + }) + }) + .catch(next) +}) + +router.get('/feed', auth.required, (req, res, next) => { + let limit = 20 + let offset = 0 + + if (typeof req.query.limit !== 'undefined') { + limit = req.query.limit + } + + if (typeof req.query.offset !== 'undefined') { + offset = req.query.offset + } + + User.findById(req.payload.id).then(user => { + if (!user) { + return res.sendStatus(401) + } + + return Promise.all([ + Article.find({author: {$in: user.following}}) + .limit(Number(limit)) + .skip(Number(offset)) + .populate('author') + .exec(), + Article.count({author: {$in: user.following}}), + ]) + .then(results => { + const articles = results[0] + const articlesCount = results[1] + + return res.json({ + articles: articles.map(article => { + return article.toJSONFor(user) + }), + articlesCount, + }) + }) + .catch(next) + }) +}) + +router.post('/', auth.required, (req, res, next) => { + User.findById(req.payload.id) + .then(user => { + if (!user) { + return res.sendStatus(401) + } + + const article = new Article(req.body.article) + + article.author = user + + return article.save().then(() => { + console.log(article.author) + return res.json({article: article.toJSONFor(user)}) + }) + }) + .catch(next) +}) + +// return a article +router.get('/:article', auth.optional, (req, res, next) => { + Promise.all([ + req.payload ? User.findById(req.payload.id) : null, + req.article.populate('author').execPopulate(), + ]) + .then(results => { + const user = results[0] + + return res.json({article: req.article.toJSONFor(user)}) + }) + .catch(next) +}) + +// update article +router.put('/:article', auth.required, (req, res, next) => { + if (req.article._id.toString() === req.payload.id.toString()) { + if (typeof req.body.article.title !== 'undefined') { + req.article.title = req.body.article.title + } + + if (typeof req.body.article.description !== 'undefined') { + req.article.description = req.body.article.description + } + + if (typeof req.body.article.body !== 'undefined') { + req.article.body = req.body.article.body + } + + return req.article + .save() + .then(article => { + return res.json({article: article.toJSONFor(article)}) + }) + .catch(next) + } else { + return res.send(403) + } +}) + +// delete article +router.delete('/:article', auth.required, (req, res) => { + User.findById(req.payload.id).then(() => { + if (req.article.author.toString() === req.payload.id.toString()) { + return req.article.remove().then(() => { + return res.sendStatus(204) + }) + } else { + return res.sendStatus(403) + } + }) +}) + +// Favorite an article +router.post('/:article/favorite', auth.required, (req, res, next) => { + const articleId = req.article._id + + User.findById(req.payload.id) + .then(user => { + if (!user) { + return res.sendStatus(401) + } + + return user.favorite(articleId).then(() => { + return req.article.updateFavoriteCount().then(article => { + return res.json({article: article.toJSONFor(user)}) + }) + }) + }) + .catch(next) +}) + +// Unfavorite an article +router.delete('/:article/favorite', auth.required, (req, res, next) => { + const articleId = req.article._id + + User.findById(req.payload.id) + .then(user => { + if (!user) { + return res.sendStatus(401) + } + + return user.unfavorite(articleId).then(() => { + return req.article.updateFavoriteCount().then(article => { + return res.json({article: article.toJSONFor(user)}) + }) + }) + }) + .catch(next) +}) + +// return an article's comments +router.get('/:article/comments', auth.optional, (req, res, next) => { + Promise.resolve(req.payload ? User.findById(req.payload.id) : null) + .then(user => { + return req.article + .populate({ + path: 'comments', + populate: { + path: 'author', + }, + options: { + sort: { + createdAt: 'desc', + }, + }, + }) + .execPopulate() + .then(() => { + return res.json({ + comments: req.article.comments.map(comment => { + return comment.toJSONFor(user) + }), + }) + }) + }) + .catch(next) +}) + +// create a new comment +router.post('/:article/comments', auth.required, (req, res, next) => { + User.findById(req.payload.id) + .then(user => { + if (!user) { + return res.sendStatus(401) + } + + const comment = new Comment(req.body.comment) + comment.article = req.article + comment.author = user + + return comment.save().then(() => { + req.article.comments.push(comment) + + return req.article.save().then(() => { + res.json({comment: comment.toJSONFor(user)}) + }) + }) + }) + .catch(next) +}) + +router.delete('/:article/comments/:comment', auth.required, (req, res) => { + if (req.comment.author.toString() === req.payload.id.toString()) { + req.article.comments.remove(req.comment._id) + req.article + .save() + .then(Comment.find({_id: req.comment._id}).remove().exec()) + .then(() => { + res.sendStatus(204) + }) + } else { + res.sendStatus(403) + } +}) + +module.exports = router diff --git a/api/routes/api/index.js b/api/routes/api/index.js new file mode 100644 index 00000000..371acaf4 --- /dev/null +++ b/api/routes/api/index.js @@ -0,0 +1,25 @@ +const router = require('express').Router() + +router.use('/', require('./users')) +router.use('/profiles', require('./profiles')) +router.use('/articles', require('./articles')) +router.use('/tags', require('./tags')) + +router.use((err, req, res, next) => { + if (err.name === 'ValidationError') { + return res.status(422).json({ + errors: Object.keys(err.errors).reduce( + (errors, key) => { + errors[key] = err.errors[key].message + + return errors + }, + {}, + ), + }) + } + + return next(err) +}) + +module.exports = router diff --git a/api/routes/api/profiles.js b/api/routes/api/profiles.js new file mode 100644 index 00000000..0defca8f --- /dev/null +++ b/api/routes/api/profiles.js @@ -0,0 +1,67 @@ +const router = require('express').Router() +const mongoose = require('mongoose') +const User = mongoose.model('User') +const auth = require('../auth') + +// Preload article objects on routes with ':username' +router.param('username', (req, res, next, username) => { + User.findOne({username}) + .then(user => { + if (!user) { + return res.sendStatus(404) + } + + req.profile = user + + return next() + }) + .catch(next) +}) + +router.get('/:username', auth.optional, (req, res, next) => { + if (req.payload) { + User.findById(req.payload.id).then(user => { + if (!user) { + return res.json({profile: req.profile.toProfileJSONFor(false)}) + } + + return res.json({profile: req.profile.toProfileJSONFor(user)}) + }) + } else { + return res.json({profile: req.profile.toProfileJSONFor(false)}) + } +}) + +router.post('/:username/follow', auth.required, (req, res, next) => { + const profileId = req.profile._id + + User.findById(req.payload.id) + .then(user => { + if (!user) { + return res.sendStatus(401) + } + + return user.follow(profileId).then(() => { + return res.json({profile: req.profile.toProfileJSONFor(user)}) + }) + }) + .catch(next) +}) + +router.delete('/:username/follow', auth.required, (req, res, next) => { + const profileId = req.profile._id + + User.findById(req.payload.id) + .then(user => { + if (!user) { + return res.sendStatus(401) + } + + return user.unfollow(profileId).then(() => { + return res.json({profile: req.profile.toProfileJSONFor(user)}) + }) + }) + .catch(next) +}) + +module.exports = router diff --git a/api/routes/api/tags.js b/api/routes/api/tags.js new file mode 100644 index 00000000..89f472fc --- /dev/null +++ b/api/routes/api/tags.js @@ -0,0 +1,16 @@ +const mongoose = require('mongoose') +const router = require('express').Router() // eslint-disable-line babel/new-cap + +const Article = mongoose.model('Article') + +// return a list of tags +router.get('/', (req, res, next) => { + Article.find() + .distinct('tagList') + .then(tags => { + return res.json({tags}) + }) + .catch(next) +}) + +module.exports = router diff --git a/api/routes/api/users.js b/api/routes/api/users.js new file mode 100644 index 00000000..88b7ade8 --- /dev/null +++ b/api/routes/api/users.js @@ -0,0 +1,88 @@ +const mongoose = require('mongoose') +const router = require('express').Router() +const passport = require('passport') +const User = mongoose.model('User') +const auth = require('../auth') + +router.get('/user', auth.required, (req, res, next) => { + User.findById(req.payload.id) + .then(user => { + if (!user) { + return res.sendStatus(401) + } + + return res.json({user: user.toAuthJSON()}) + }) + .catch(next) +}) + +router.put('/user', auth.required, (req, res, next) => { + User.findById(req.payload.id) + .then(user => { + if (!user) { + return res.sendStatus(401) + } + + // only update fields that were actually passed... + if (typeof req.body.user.username !== 'undefined') { + user.username = req.body.user.username + } + if (typeof req.body.user.email !== 'undefined') { + user.email = req.body.user.email + } + if (typeof req.body.user.bio !== 'undefined') { + user.bio = req.body.user.bio + } + if (typeof req.body.user.image !== 'undefined') { + user.image = req.body.user.image + } + if (typeof req.body.user.password !== 'undefined') { + user.setPassword(req.body.user.password) + } + + return user.save().then(() => { + return res.json({user: user.toAuthJSON()}) + }) + }) + .catch(next) +}) + +router.post('/users/login', (req, res, next) => { + if (!req.body.user.email) { + return res.status(422).json({errors: {email: "can't be blank"}}) + } + + if (!req.body.user.password) { + return res.status(422).json({errors: {password: "can't be blank"}}) + } + + passport.authenticate('local', {session: false}, (err, user, info) => { + if (err) { + return next(err) + } + + if (user) { + user.token = user.generateJWT() + return res.json({user: user.toAuthJSON()}) + } else { + return res.status(422).json(info) + } + })(req, res, next) +}) + +router.post('/users', (req, res, next) => { + const user = new User() + + user.username = req.body.user.username + user.email = req.body.user.email + user.setPassword(req.body.user.password) + + user + .save() + .then(() => { + return res.json({user: user.toAuthJSON()}) + }) + .catch(next) +}) + +module.exports = router diff --git a/api/routes/auth.js b/api/routes/auth.js new file mode 100644 index 00000000..649acb31 --- /dev/null +++ b/api/routes/auth.js @@ -0,0 +1,29 @@ +const jwt = require('express-jwt') +const secret = require('../config').secret + +function getTokenFromHeader(req) { + if ( + req.headers.authorization && + req.headers.authorization.split(' ')[0] === 'Token' + ) { + return req.headers.authorization.split(' ')[1] + } + + return null +} + +const auth = { + required: jwt({ + secret, + userProperty: 'payload', + getToken: getTokenFromHeader, + }), + optional: jwt({ + secret, + userProperty: 'payload', + credentialsRequired: false, + getToken: getTokenFromHeader, + }), +} + +module.exports = auth diff --git a/api/routes/index.js b/api/routes/index.js new file mode 100644 index 00000000..018b0fa2 --- /dev/null +++ b/api/routes/index.js @@ -0,0 +1,5 @@ +const router = require('express').Router() + +router.use('/api', require('./api')) + +module.exports = router diff --git a/api/yarn.lock b/api/yarn.lock new file mode 100644 index 00000000..1bb5a820 --- /dev/null +++ b/api/yarn.lock @@ -0,0 +1,1738 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accepts@~1.2.12: + version "1.2.13" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.2.13.tgz#e5f1f3928c6d95fd96558c36ec3d9d0de4a6ecea" + dependencies: + mime-types "~2.1.6" + negotiator "0.5.3" + +accepts@~1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +ajv-keywords@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + +ajv@^4.7.0: + version "4.11.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.3.tgz#ce30bdb90d1254f762c75af915fb3a63e7183d22" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +async@1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^0.9.0: + version "0.9.2" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" + +async@^2.0.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4" + dependencies: + lodash "^4.14.0" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +babel-code-frame@^6.16.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +base64-url@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-url/-/base64-url-1.2.1.tgz#199fd661702a0e7b7dcae6e0698bb089c52f6d78" + +base64url@2.0.0, base64url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" + +basic-auth@~1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.0.4.tgz#030935b01de7c9b94a824b29f3fccb750d3a5290" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +bl@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.0.3.tgz#fc5421a28fd4226036c3b3891a66a25bc64d226e" + dependencies: + readable-stream "~2.0.5" + +bluebird@2.10.2: + version "2.10.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.10.2.tgz#024a5517295308857f14f91f1106fc3b555f446b" + +body-parser@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.15.0.tgz#8168abaeaf9e77e300f7b3aef4df4b46e9b21b35" + dependencies: + bytes "2.2.0" + content-type "~1.0.1" + debug "~2.2.0" + depd "~1.1.0" + http-errors "~1.4.0" + iconv-lite "0.4.13" + on-finished "~2.3.0" + qs "6.1.0" + raw-body "~2.1.5" + type-is "~1.6.11" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +brace-expansion@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +bson@0.4.21, bson@~0.4.21: + version "0.4.21" + resolved "https://registry.yarnpkg.com/bson/-/bson-0.4.21.tgz#b8eae38c5aa94f7b8e64e8cfed0f42e58308ed95" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + +buffer-shims@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + +"bufferjs@>= 2.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/bufferjs/-/bufferjs-3.0.1.tgz#0692e829cb10a10550e647390b035eb06c38e8ef" + +"bufferstream@>= 0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/bufferstream/-/bufferstream-0.6.2.tgz#a5f27e10d3c760084d14b35126615007319e3731" + dependencies: + bufferjs ">= 2.0.0" + optionalDependencies: + buffertools ">= 1.0.3" + +"buffertools@>= 1.0.3": + version "2.1.4" + resolved "https://registry.yarnpkg.com/buffertools/-/buffertools-2.1.4.tgz#62d4e1584c0090a0c7d3587f25934a84b8b38de4" + +bytes@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.2.0.tgz#fd35464a403f6f9117c2de3609ecff9cae000588" + +bytes@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +circular-json@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.4.6: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +content-disposition@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" + +content-type@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.1.5.tgz#6ab9948a4b1ae21952cd2588530a4722d4044d7c" + +cookie@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.2.3.tgz#1a59536af68537a21178a01346f87cb059d2ae5c" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cors@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.7.1.tgz#3c2e50a58af9ef8c89bee21226b099be1f02739b" + dependencies: + vary "^1" + +crc@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.0.tgz#4258e351613a74ef1153dfcb05e820c3e9715d7f" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +d@^0.1.1, d@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" + dependencies: + es5-ext "~0.10.2" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +debug@2.2.0, debug@^2.1.1, debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +depd@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +doctrine@^1.2.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +dotenv@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-2.0.0.tgz#bd759c357aaa70365e01c96b7b0bec08a6e0d949" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ecdsa-sig-formatter@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" + dependencies: + base64url "^2.0.0" + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +ejs@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.4.1.tgz#82e15b1b2a1f948b18097476ba2bd7c66f4d1566" + +errorhandler@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.4.3.tgz#b7b70ed8f359e9db88092f2d20c0f831420ad83f" + dependencies: + accepts "~1.3.0" + escape-html "~1.0.3" + +es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7: + version "0.10.12" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac" + dependencies: + d "^0.1.1" + es5-ext "^0.10.7" + es6-symbol "3" + +es6-map@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-set "~0.1.3" + es6-symbol "~3.1.0" + event-emitter "~0.3.4" + +es6-promise@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" + +es6-set@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-symbol "3" + event-emitter "~0.3.4" + +es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + +es6-weak-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81" + dependencies: + d "^0.1.1" + es5-ext "^0.10.8" + es6-iterator "2" + es6-symbol "3" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.15.0.tgz#bdcc6a6c5ffe08160e7b93c066695362a91e30f2" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.4.6" + debug "^2.1.1" + doctrine "^1.2.2" + escope "^3.6.0" + espree "^3.4.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.14.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~2.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.0.tgz#41656fa5628e042878025ef467e78f125cb86e1d" + dependencies: + acorn "4.0.4" + acorn-jsx "^3.0.0" + +esprima@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + +esrecurse@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + dependencies: + estraverse "~4.1.0" + object-assign "^4.0.1" + +estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +estraverse@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" + +event-emitter@~0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5" + dependencies: + d "~0.1.1" + es5-ext "~0.10.7" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +express-jwt@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/express-jwt/-/express-jwt-3.3.0.tgz#d10e17244225b1968d20137ff77fc7488c88f494" + dependencies: + async "^0.9.0" + express-unless "^0.3.0" + jsonwebtoken "^5.0.0" + lodash "~3.10.1" + +express-session@1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.13.0.tgz#8ac3b5c0188b48382851d88207b8e7746efb4011" + dependencies: + cookie "0.2.3" + cookie-signature "1.0.6" + crc "3.4.0" + debug "~2.2.0" + depd "~1.1.0" + on-headers "~1.0.1" + parseurl "~1.3.0" + uid-safe "~2.0.0" + utils-merge "1.0.0" + +express-unless@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/express-unless/-/express-unless-0.3.0.tgz#5c795e7392571512dd28f520b3857a52b21261a2" + +express@4.13.4: + version "4.13.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.13.4.tgz#3c0b76f3c77590c8345739061ec0bd3ba067ec24" + dependencies: + accepts "~1.2.12" + array-flatten "1.1.1" + content-disposition "0.5.1" + content-type "~1.0.1" + cookie "0.1.5" + cookie-signature "1.0.6" + debug "~2.2.0" + depd "~1.1.0" + escape-html "~1.0.3" + etag "~1.7.0" + finalhandler "0.4.1" + fresh "0.3.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.0.10" + qs "4.0.0" + range-parser "~1.0.3" + send "0.13.1" + serve-static "~1.10.2" + type-is "~1.6.6" + utils-merge "1.0.0" + vary "~1.0.1" + +extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +finalhandler@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.4.1.tgz#85a17c6c59a94717d262d61230d4b0ebe3d4a14d" + dependencies: + debug "~2.2.0" + escape-html "~1.0.3" + on-finished "~2.3.0" + unpipe "~1.0.0" + +flat-cache@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~1.0.0-rc3: + version "1.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" + dependencies: + async "^2.0.1" + combined-stream "^1.0.5" + mime-types "^2.1.11" + +forwarded@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" + +fresh@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +getpass@^0.1.1: + version "0.1.6" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" + dependencies: + assert-plus "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^9.14.0: + version "9.14.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +hawk@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hooks-fixed@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hooks-fixed/-/hooks-fixed-1.1.0.tgz#0e8c15336708e6611185fe390b44687dd5230dbb" + +http-errors@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.3.1.tgz#197e22cdebd4198585e8694ef6786197b91ed942" + dependencies: + inherits "~2.0.1" + statuses "1" + +http-errors@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.4.0.tgz#6c0242dea6b3df7afda153c71089b31c6e82aabf" + dependencies: + inherits "2.0.1" + statuses ">= 1.2.1 < 2" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.13: + version "0.4.13" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" + +ignore@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1, inherits@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" + +ipaddr.js@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.0.5.tgz#5fa78cf301b825c78abc3042d812723049ea23c7" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isemail@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jodid25519@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" + dependencies: + jsbn "~0.1.0" + +joi@^6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06" + dependencies: + hoek "2.x.x" + isemail "1.x.x" + moment "2.x.x" + topo "1.x.x" + +js-tokens@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" + +js-yaml@^3.5.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628" + dependencies: + argparse "^1.0.7" + esprima "^3.1.1" + +jsbn@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + +jsonwebtoken@7.1.9: + version "7.1.9" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.1.9.tgz#847804e5258bec5a9499a8dc4a5e7a3bae08d58a" + dependencies: + joi "^6.10.1" + jws "^3.1.3" + lodash.once "^4.0.0" + ms "^0.7.1" + xtend "^4.0.1" + +jsonwebtoken@^5.0.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-5.7.0.tgz#1c90f9a86ce5b748f5f979c12b70402b4afcddb4" + dependencies: + jws "^3.0.0" + ms "^0.7.1" + xtend "^4.0.1" + +jsprim@^1.2.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" + dependencies: + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +jwa@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" + dependencies: + base64url "2.0.0" + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.9" + safe-buffer "^5.0.1" + +jws@^3.0.0, jws@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" + dependencies: + base64url "^2.0.0" + jwa "^1.1.4" + safe-buffer "^5.0.1" + +kareem@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.0.1.tgz#7805d215bb53214ec3af969a1d0b1f17e3e7b95c" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lodash.foreach@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + +lodash.get@^4.0.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.3.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +lodash@~3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +method-override@2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/method-override/-/method-override-2.3.5.tgz#2cd5cdbff00c3673d7ae345119a812a5d95b8c8e" + dependencies: + debug "~2.2.0" + methods "~1.1.1" + parseurl "~1.3.0" + vary "~1.0.1" + +methods@1.1.2, methods@~1.1.1, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +mime-db@~1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" + +mime-types@^2.1.11, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.6, mime-types@~2.1.7: + version "2.1.14" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" + dependencies: + mime-db "~1.26.0" + +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +minimatch@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +moment@2.x.x: + version "2.17.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" + +mongodb-core@1.3.9: + version "1.3.9" + resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-1.3.9.tgz#39db2f5211fe8fc8a7a618926b079081147d4e6e" + dependencies: + bson "~0.4.21" + require_optional "~1.0.0" + +mongodb@2.1.10: + version "2.1.10" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.1.10.tgz#d24a7ab58516cbbeee7c256d4fb00a774f8e4d4f" + dependencies: + es6-promise "3.0.2" + mongodb-core "1.3.9" + readable-stream "1.0.31" + +mongoose-unique-validator@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mongoose-unique-validator/-/mongoose-unique-validator-1.0.2.tgz#f12bb892918bd95e19cb62beb3c00b044ab25ea0" + dependencies: + lodash.foreach "^4.1.0" + lodash.get "^4.0.2" + +mongoose@4.4.10: + version "4.4.10" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.4.10.tgz#6277f2f918040868f4aecfbc2833153efb92472f" + dependencies: + async "1.5.2" + bson "0.4.21" + hooks-fixed "1.1.0" + kareem "1.0.1" + mongodb "2.1.10" + mpath "0.2.1" + mpromise "0.5.5" + mquery "1.10.0" + ms "0.7.1" + muri "1.1.0" + regexp-clone "0.0.1" + sliced "1.0.1" + +morgan@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.7.0.tgz#eb10ca8e50d1abe0f8d3dad5c0201d052d981c62" + dependencies: + basic-auth "~1.0.3" + debug "~2.2.0" + depd "~1.1.0" + on-finished "~2.3.0" + on-headers "~1.0.1" + +mpath@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.2.1.tgz#3a4e829359801de96309c27a6b2e102e89f9e96e" + +mpromise@0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mpromise/-/mpromise-0.5.5.tgz#f5b24259d763acc2257b0a0c8c6d866fd51732e6" + +mquery@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-1.10.0.tgz#8603f02b0b524d17ac0539a85996124ee17b7cb3" + dependencies: + bluebird "2.10.2" + debug "2.2.0" + regexp-clone "0.0.1" + sliced "0.0.5" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +muri@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/muri/-/muri-1.1.0.tgz#a3a6d74e68a880f433a249a74969cbb665cc0add" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +negotiator@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.5.3.tgz#269d5c476810ec92edbe7b6c2f28316384f9a7e8" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-uuid@~1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.0: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +parseurl@~1.3.0, parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +passport-local@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" + dependencies: + passport-strategy "1.x.x" + +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + +passport@0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.3.2.tgz#9dd009f915e8fe095b0124a01b8f82da07510102" + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +proxy-addr@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.0.10.tgz#0d40a82f801fc355567d2ecb65efe3f077f121c5" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.0.5" + +qs@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607" + +qs@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.1.0.tgz#ec1d1626b24278d99f0fdf4549e524e24eceeb26" + +qs@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.0.2.tgz#88c68d590e8ed56c76c79f352c17b982466abfcd" + +range-parser@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.0.3.tgz#6872823535c692e2c2a0103826afd82c2e0ff175" + +raw-body@~2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" + dependencies: + bytes "2.4.0" + iconv-lite "0.4.13" + unpipe "1.0.0" + +readable-stream@1.0.31: + version "1.0.31" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.31.tgz#8f2502e0bc9e3b0da1b94520aabb4e2603ecafae" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readable-stream@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +regexp-clone@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589" + +request@2.69.0: + version "2.69.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.69.0.tgz#cf91d2e000752b1217155c005241911991a2346a" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + bl "~1.0.0" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~1.0.0-rc3" + har-validator "~2.0.6" + hawk "~3.1.0" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.7" + oauth-sign "~0.8.0" + qs "~6.0.2" + stringstream "~0.0.4" + tough-cookie "~2.2.0" + tunnel-agent "~0.4.1" + +require-uncached@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +require_optional@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.0.tgz#52a86137a849728eb60a55533617f8f914f59abf" + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + +resolve@^1.1.6: + version "1.2.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +rimraf@^2.2.8: + version "2.5.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" + dependencies: + glob "^7.0.5" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +safe-buffer@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" + +semver@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.13.1.tgz#a30d5f4c82c8a9bae9ad00a1d9b1bdbe6f199ed7" + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.3.1" + mime "1.3.4" + ms "0.7.1" + on-finished "~2.3.0" + range-parser "~1.0.3" + statuses "~1.2.1" + +send@0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.13.2.tgz#765e7607c8055452bba6f0b052595350986036de" + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.3.1" + mime "1.3.4" + ms "0.7.1" + on-finished "~2.3.0" + range-parser "~1.0.3" + statuses "~1.2.1" + +serve-static@~1.10.2: + version "1.10.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.10.3.tgz#ce5a6ecd3101fed5ec09827dac22a9c29bfb0535" + dependencies: + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.13.2" + +shelljs@^0.7.5: + version "0.7.6" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +sliced@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-0.0.5.tgz#5edc044ca4eb6f7816d50ba2fc63e25d8fe4707f" + +sliced@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + +slug@0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/slug/-/slug-0.9.1.tgz#af08f608a7c11516b61778aa800dce84c518cfda" + dependencies: + unicode ">= 0.3.1" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.2.tgz#d5a804ce22695515638e798dbe23273de070a5fa" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jodid25519 "^1.0.0" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +statuses@1, "statuses@>= 1.2.1 < 2": + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +statuses@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.2.1.tgz#dded45cc18256d51ed40aec142489d5c61026d28" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +topo@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5" + dependencies: + hoek "2.x.x" + +tough-cookie@~2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.2.2.tgz#c83a1830f4e5ef0b93ef2a3488e724f8de016ac7" + +tryit@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.11, type-is@~1.6.6: + version "1.6.14" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.13" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uid-safe@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.0.0.tgz#a7f3c6ca64a1f6a5d04ec0ef3e4c3d5367317137" + dependencies: + base64-url "1.2.1" + +underscore@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +"unicode@>= 0.3.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/unicode/-/unicode-0.6.1.tgz#ec69e3c4537e2b9650b826133bcb068f0445d0bc" + dependencies: + bufferstream ">= 0.6.2" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + +vary@^1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" + +vary@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.0.1.tgz#99e4981566a286118dfb2b817357df7993376d10" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +xtend@^4.0.0, xtend@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" diff --git a/client/README.md b/client/README.md new file mode 100644 index 00000000..e1bf7369 --- /dev/null +++ b/client/README.md @@ -0,0 +1,57 @@ +# ![React + Redux Example App](project-logo.png) + +> Example React + Redux codebase that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) spec and API. + +# Redux codebase containing real world examples (CRUD, auth, advanced patterns, etc) +Originally created for this [GH issue](https://github.com/reactjs/redux/issues/1353). The codebase is now feature complete and the RFC is open. **Your input is greatly appreciated; please submit bug fixes via pull requests & feedback via issues**. + +We're currently working on some docs for the codebase (explaining where functionality is located, how it works, etc) but most things should be self explanatory if you have a minimal understanding of React/Redux. + +## Getting started + +You can view a live demo over at https://react-redux.realworld.io/ + +To get the frontend running locally: + +- Clone this repo +- `npm install` to install all req'd dependencies +- `npm start` to start the local server (this project uses create-react-app) + +For convenience, we have a live API server running at https://conduit.productionready.io/api for the application to make requests against. You can view [the API spec here](https://github.com/GoThinkster/productionready/blob/master/API.md) which contains all routes & responses for the server. We'll release the backend code in a few weeks should anyone be interested in it. + +## Functionality overview + +The example application is a social blogging site (i.e. a Medium.com clone) called "Conduit". It uses a custom API for all requests, including authentication. You can view a live demo over at https://redux.productionready.io/ + +**General functionality:** + +- Authenticate users via JWT (login/signup pages + logout button on settings page) +- CRU* users (sign up & settings page - no deleting required) +- CRUD Articles +- CR*D Comments on articles (no updating required) +- GET and display paginated lists of articles +- Favorite articles +- Follow other users + +**The general page breakdown looks like this:** + +- Home page (URL: /#/ ) + - List of tags + - List of articles pulled from either Feed, Global, or by Tag + - Pagination for list of articles +- Sign in/Sign up pages (URL: /#/login, /#/register ) + - Use JWT (store the token in localStorage) +- Settings page (URL: /#/settings ) +- Editor page to create/edit articles (URL: /#/editor, /#/editor/article-slug-here ) +- Article page (URL: /#/article/article-slug-here ) + - Delete article button (only shown to article's author) + - Render markdown from server client side + - Comments section at bottom of page + - Delete comment button (only shown to comment's author) +- Profile page (URL: /#/@username, /#/@username/favorites ) + - Show basic user info + - List of articles populated from author's created articles or author's favorited articles + +
+ +[![Brought to you by Thinkster](https://raw.githubusercontent.com/gothinkster/realworld/master/media/end.png)](https://thinkster.io) diff --git a/client/package.json b/client/package.json new file mode 100644 index 00000000..a359f0f0 --- /dev/null +++ b/client/package.json @@ -0,0 +1,29 @@ +{ + "name": "react-redux-realworld-example-app", + "version": "0.1.0", + "private": true, + "devDependencies": { + "react-scripts": "0.6.1" + }, + "license": "MIT", + "repository": "git@github.com:kentcdodds/testing-workshop.git", + "dependencies": { + "history": "^4.3.0", + "marked": "^0.3.6", + "react": "^15.3.2", + "react-dom": "^15.3.2", + "react-redux": "^4.4.5", + "react-router": "^2.8.1", + "redux": "^3.6.0", + "redux-logger": "^2.7.4", + "superagent": "^2.3.0", + "superagent-promise": "^1.1.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "author": "Thinkster (https://github.com/gothinkster)" +} diff --git a/client/project-logo.png b/client/project-logo.png new file mode 100644 index 00000000..901b3b61 Binary files /dev/null and b/client/project-logo.png differ diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 00000000..5c125de5 Binary files /dev/null and b/client/public/favicon.ico differ diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 00000000..7253c806 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + Conduit + + +
+ + + diff --git a/client/src/agent.js b/client/src/agent.js new file mode 100644 index 00000000..eca2db6c --- /dev/null +++ b/client/src/agent.js @@ -0,0 +1,92 @@ +import superagentPromise from 'superagent-promise' +import _superagent from 'superagent' + +const superagent = superagentPromise(_superagent, global.Promise) + +// const API_ROOT = 'https://conduit.productionready.io/api'; +const API_ROOT = 'http://localhost:3000/api' + +const encode = encodeURIComponent +const responseBody = res => res.body + +let token = null +const tokenPlugin = req => { + if (token) { + req.set('authorization', `Token ${token}`) + } +} + +const requests = { + del: url => + superagent.del(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody), + get: url => + superagent.get(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody), + put: (url, body) => + superagent + .put(`${API_ROOT}${url}`, body) + .use(tokenPlugin) + .then(responseBody), + post: (url, body) => + superagent + .post(`${API_ROOT}${url}`, body) + .use(tokenPlugin) + .then(responseBody), +} + +const Auth = { + current: () => requests.get('/user'), + login: (email, password) => + requests.post('/users/login', {user: {email, password}}), + register: (username, email, password) => + requests.post('/users', {user: {username, email, password}}), + save: user => requests.put('/user', {user}), +} + +const Tags = { + getAll: () => requests.get('/tags'), +} + +const limit = (count, p) => `limit=${count}&offset=${p ? p * count : 0}` +const omitSlug = article => Object.assign({}, article, {slug: undefined}) +const Articles = { + all: page => requests.get(`/articles?${limit(10, page)}`), + byAuthor: (author, page) => + requests.get(`/articles?author=${encode(author)}&${limit(5, page)}`), + byTag: (tag, page) => + requests.get(`/articles?tag=${encode(tag)}&${limit(10, page)}`), + del: slug => requests.del(`/articles/${slug}`), + favorite: slug => requests.post(`/articles/${slug}/favorite`), + favoritedBy: (author, page) => + requests.get(`/articles?favorited=${encode(author)}&${limit(5, page)}`), + feed: () => requests.get('/articles/feed?limit=10&offset=0'), + get: slug => requests.get(`/articles/${slug}`), + unfavorite: slug => requests.del(`/articles/${slug}/favorite`), + update: article => + requests.put(`/articles/${article.slug}`, {article: omitSlug(article)}), + create: article => requests.post('/articles', {article}), +} + +const Comments = { + create: (slug, comment) => + requests.post(`/articles/${slug}/comments`, {comment}), + delete: (slug, commentId) => + requests.del(`/articles/${slug}/comments/${commentId}`), + forArticle: slug => requests.get(`/articles/${slug}/comments`), +} + +const Profile = { + follow: username => requests.post(`/profiles/${username}/follow`), + get: username => requests.get(`/profiles/${username}`), + unfollow: username => requests.del(`/profiles/${username}/follow`), +} + +export default { + Articles, + Auth, + Comments, + Profile, + Tags, + setToken: _token => { + token = _token + }, +} diff --git a/client/src/components/App.js b/client/src/components/App.js new file mode 100644 index 00000000..37ff1180 --- /dev/null +++ b/client/src/components/App.js @@ -0,0 +1,63 @@ +import agent from '../agent' +import Header from './Header' +import React from 'react' +import {connect} from 'react-redux' + +const mapStateToProps = state => ({ + appLoaded: state.common.appLoaded, + appName: state.common.appName, + currentUser: state.common.currentUser, + redirectTo: state.common.redirectTo, +}) + +const mapDispatchToProps = dispatch => ({ + onLoad: (payload, token) => + dispatch({type: 'APP_LOAD', payload, token, skipTracking: true}), + onRedirect: () => dispatch({type: 'REDIRECT'}), +}) + +class App extends React.Component { + componentWillReceiveProps(nextProps) { + if (nextProps.redirectTo) { + this.context.router.replace(nextProps.redirectTo) + this.props.onRedirect() + } + } + + componentWillMount() { + const token = window.localStorage.getItem('jwt') + if (token) { + agent.setToken(token) + } + + this.props.onLoad(token ? agent.Auth.current() : null, token) + } + + render() { + if (this.props.appLoaded) { + return ( +
+
+ {this.props.children} +
+ ) + } + return ( +
+
+
+ ) + } +} + +App.contextTypes = { + router: React.PropTypes.object.isRequired, +} + +export default connect(mapStateToProps, mapDispatchToProps)(App) diff --git a/client/src/components/Article/ArticleActions.js b/client/src/components/Article/ArticleActions.js new file mode 100644 index 00000000..df5cfd09 --- /dev/null +++ b/client/src/components/Article/ArticleActions.js @@ -0,0 +1,37 @@ +import {Link} from 'react-router' +import React from 'react' +import agent from '../../agent' +import {connect} from 'react-redux' + +const mapDispatchToProps = dispatch => ({ + onClickDelete: payload => dispatch({type: 'DELETE_ARTICLE', payload}), +}) + +const ArticleActions = props => { + const article = props.article + const del = () => { + props.onClickDelete(agent.Articles.del(article.slug)) + } + if (props.canModify) { + return ( + + + + Edit Article + + + + + + ) + } + + return +} + +export default connect(() => ({}), mapDispatchToProps)(ArticleActions) diff --git a/client/src/components/Article/ArticleMeta.js b/client/src/components/Article/ArticleMeta.js new file mode 100644 index 00000000..7f1ad769 --- /dev/null +++ b/client/src/components/Article/ArticleMeta.js @@ -0,0 +1,27 @@ +import ArticleActions from './ArticleActions' +import {Link} from 'react-router' +import React from 'react' + +const ArticleMeta = props => { + const article = props.article + return ( +
+ + + + +
+ + {article.author.username} + + + {new Date(article.createdAt).toDateString()} + +
+ + +
+ ) +} + +export default ArticleMeta diff --git a/client/src/components/Article/Comment.js b/client/src/components/Article/Comment.js new file mode 100644 index 00000000..bbbb1a31 --- /dev/null +++ b/client/src/components/Article/Comment.js @@ -0,0 +1,31 @@ +import DeleteButton from './DeleteButton' +import {Link} from 'react-router' +import React from 'react' + +const Comment = props => { + const comment = props.comment + const show = props.currentUser && + props.currentUser.username === comment.author.username + return ( +
+
+

{comment.body}

+
+
+ + + +   + + {comment.author.username} + + + {new Date(comment.createdAt).toDateString()} + + +
+
+ ) +} + +export default Comment diff --git a/client/src/components/Article/CommentContainer.js b/client/src/components/Article/CommentContainer.js new file mode 100644 index 00000000..4bad55de --- /dev/null +++ b/client/src/components/Article/CommentContainer.js @@ -0,0 +1,42 @@ +import CommentInput from './CommentInput' +import CommentList from './CommentList' +import {Link} from 'react-router' +import React from 'react' + +const CommentContainer = props => { + if (props.currentUser) { + return ( +
+
+ + +
+ + +
+ ) + } else { + return ( +
+

+ Sign in +  or  + sign up +  to add comments on this article. +

+ + +
+ ) + } +} + +export default CommentContainer diff --git a/client/src/components/Article/CommentInput.js b/client/src/components/Article/CommentInput.js new file mode 100644 index 00000000..7754d0ca --- /dev/null +++ b/client/src/components/Article/CommentInput.js @@ -0,0 +1,56 @@ +import React from 'react' +import agent from '../../agent' +import {connect} from 'react-redux' + +const mapDispatchToProps = dispatch => ({ + onSubmit: payload => dispatch({type: 'ADD_COMMENT', payload}), +}) + +class CommentInput extends React.Component { + constructor() { + super() + this.state = { + body: '', + } + + this.setBody = ev => { + this.setState({body: ev.target.value}) + } + + this.createComment = ev => { + ev.preventDefault() + const payload = agent.Comments.create(this.props.slug, { + body: this.state.body, + }) + this.setState({body: ''}) + this.props.onSubmit(payload) + } + } + + render() { + return ( +
+
+