diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..a4ff3c30 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +IMGUR_CLIENT_ID = \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..6feca7ec --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: NODE_ENV=production node app.js \ No newline at end of file diff --git a/app.js b/app.js index f70e64d6..d86ca4a0 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,37 @@ const express = require('express') +const exphbs = require('express-handlebars') +const flash = require('connect-flash') +const session = require('express-session') +const methodOverride = require('method-override') +const bodyParser = require('body-parser') +const helpers = require('./_helpers') +const handlebarsHelpers = require('./helpers/handlebarsHelpers') +if (process.env.NODE_ENV !== 'production') { + require('dotenv').config() +} +const passport = require('./config/passport') const app = express() -const port = 3000 +const port = process.env.PORT || 3000 +app.engine('handlebars', exphbs({ defaultLayout: 'main', helpers: handlebarsHelpers })) +app.set('view engine', 'handlebars') +app.use(express.static('public')) +app.use(methodOverride('_method')) +app.use(express.urlencoded({extended: true})) +app.use(bodyParser.json()) +app.use(session({ secret: 'secret', resave: false, saveUninitialized: false })) +app.use(passport.initialize()) +app.use(passport.session()) +app.use(flash()) + +app.use((req, res, next) => { + res.locals.user = helpers.getUser(req) + res.locals.success_messages = req.flash('success_messages') + res.locals.error_messages = req.flash('error_messages') + next() +}) + +app.use('/upload', express.static(__dirname + '/upload')) app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) }) diff --git a/config/config.json b/config/config.json index 2b8be8e7..e13f7865 100644 --- a/config/config.json +++ b/config/config.json @@ -7,11 +7,14 @@ "dialect": "mysql" }, "production": { - "username": "root", - "password": null, - "database": "database_production", - "host": "127.0.0.1", - "dialect": "mysql" + "use_env_variable": "CLEARDB_DATABASE_URL", + "dialect": "postgres", + "protocol": "postgres", + "dialectOptions": { + "ssl": { + "rejectUnauthorized": false + } + } }, "travis": { "username": "travis", diff --git a/config/passport.js b/config/passport.js new file mode 100644 index 00000000..40d5d2ef --- /dev/null +++ b/config/passport.js @@ -0,0 +1,69 @@ +const passport = require('passport') +const LocalStrategy = require('passport-local') +const bcrypt = require('bcryptjs') +const db = require('../models') +const User = db.User +const Restaurant = db.Restaurant + +// setup passport strategy +passport.use(new LocalStrategy( + // customize user field + { + usernameField: 'email', + passwordField: 'password', + passReqToCallback: true + }, + // authenticate user + (req, username, password, cb) => { + User.findOne({ where: { email: username } }).then(user => { + if (!user) return cb(null, false, req.flash('error_messages', '帳號或密碼輸入錯誤')) + if (!bcrypt.compareSync(password, user.password)) return cb(null, false, req.flash('error_messages', '帳號或密碼輸入錯誤!')) + return cb(null, user) + }) + } +)) + +// serialize and deserialize user +passport.serializeUser((user, cb) => { + cb(null, user.id) +}) +passport.deserializeUser((id, cb) => { + User.findByPk(id,{ + include: [ + { model: Restaurant, as: 'FavoritedRestaurants' }, + { model: Restaurant, as: 'LikedRestaurants' }, + { model: User, as: 'Followers' }, + { model: User, as: 'Followings' } + ] + }).then(user => { + user = user.toJSON() // 此處與影片示範不同 + return cb(null, user) + }) +}) + +// JWT +const jwt = require('jsonwebtoken') +const passportJWT = require('passport-jwt') +const ExtractJwt = passportJWT.ExtractJwt +const JwtStrategy = passportJWT.Strategy + +let jwtOptions = {} +jwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken() +jwtOptions.secretOrKey = process.env.JWT_SECRET + +let strategy = new JwtStrategy(jwtOptions, function (jwt_payload, next) { + User.findByPk(jwt_payload.id, { + include: [ + { model: db.Restaurant, as: 'FavoritedRestaurants' }, + { model: db.Restaurant, as: 'LikedRestaurants' }, + { model: User, as: 'Followers' }, + { model: User, as: 'Followings' } + ] + }).then(user => { + if (!user) return next(null, false) + return next(null, user) + }) +}) +passport.use(strategy) + +module.exports = passport \ No newline at end of file diff --git a/controllers/adminController.js b/controllers/adminController.js new file mode 100644 index 00000000..be4c4084 --- /dev/null +++ b/controllers/adminController.js @@ -0,0 +1,102 @@ +const fs = require('fs') +const imgur = require('imgur-node-api') +const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID +const db = require('../models') +const Restaurant = db.Restaurant +const User = db.User +const Category = db.Category +const adminService = require('../services/adminService.js') + +const adminController = { + getRestaurants: (req, res) => { + adminService.getRestaurants(req, res, (data) => { + return res.render('admin/restaurants', data) + }) + }, + + createRestaurant: (req, res) => { + Category.findAll({ + raw: true, + nest: true + }).then(categories => { + return res.render('admin/create', { categories }) + }) + }, + + postRestaurant: (req, res) => { + adminService.postRestaurant(req, res, (data) => { + if (data['status'] === 'error') { + req.flash('error_messages', data['message']) + return res.redirect('back') + } + req.flash('success_messages', data['message']) + res.redirect('/admin/restaurants') + }) + }, + + getRestaurant: (req, res) => { + adminService.getRestaurant(req, res, data => { + return res.render('admin/restaurant', { restaurant: data.restaurant }) + }) + }, + + editRestaurant: (req, res) => { + Category.findAll({ + raw: true, + nest: true + }).then(categories => { + return Restaurant.findByPk(req.params.id).then(restaurant => { + return res.render('admin/create', { + categories, + restaurant: restaurant.toJSON() + }) + }) + }) + }, + + putRestaurant: (req, res) => { + adminService.putRestaurant(req, res, data => { + if(data['status'] === 'error'){ + req.flash('error_messages', data['message']) + return res.redirect('back') + } + req.flash('success_messages', data['message']) + return res.redirect('/admin/restaurants') + }) + }, + + deleteRestaurant: (req, res) => { + adminService.deleteRestaurant(req, res, (data) => { + if (data['status'] === 'success') { + return res.redirect('/admin/restaurants') + } + }) + }, + + getUsers: (req, res) => { + return User.findAll({ raw: true, nest: true }) + .then(users => res.render('admin/users', { users })) + }, + + toggleAdmin: async (req, res) => { + //判斷是否能變更管理權限 (自己不能變更自己) + // if (Number(req.params.id) === req.user.id) { + // req.flash('error_messages','禁止變更管理者權限') + // return res.redirect('/admin/users') + // } + //變更管理權限 + return User.findByPk(req.params.id) + .then(async (user) => { + if (user.email === 'root@example.com') { + req.flash('error_messages','禁止變更管理者權限') + return res.redirect('back') + } + user.isAdmin ? await user.update({isAdmin: false }) : await user.update({isAdmin: true }) + req.flash('success_messages','使用者權限變更成功') + return res.redirect('/admin/users') + }) + } +} + + +module.exports = adminController diff --git a/controllers/api/adminController.js b/controllers/api/adminController.js new file mode 100644 index 00000000..47c4e7ae --- /dev/null +++ b/controllers/api/adminController.js @@ -0,0 +1,53 @@ +const db = require('../../models') +const Restaurant = db.Restaurant +const Category = db.Category +const adminService = require('../../services/adminService') + +const adminController = { + getRestaurants: (req, res) => { + adminService.getRestaurants(req, res, (data) => { + return res.json(data) + }) + }, + getRestaurant: (req, res) => { + adminService.getRestaurant(req, res, (data) => { + return res.json(data) + }) + }, + getCategories:(req, res) => { + adminService.getCategories(req, res, (data) => { + return res.json(data) + }) + }, + deleteRestaurant: (req, res) => { + adminService.deleteRestaurant(req, res, (data) => { + return res.json(data) + }) + }, + postRestaurant: (req, res) => { + adminService.postRestaurant(req, res, (data) => { + return res.json(data) + }) + }, + putRestaurant: (req, res) => { + adminService.putRestaurant(req, res, (data) => { + return res.json(data) + }) + }, + postCategory: (req, res) => { + adminService.postCategory(req, res, data => { + return res.json(data) + }) + }, + putCategory: (req, res) => { + adminService.putCategory(req, res, data => { + return res.json(data) + }) + }, + deleteCategory: (req, res) => { + adminService.deleteCategory(req, res, data => { + return res.json(data) + }) + } +} +module.exports = adminController \ No newline at end of file diff --git a/controllers/api/userController.js b/controllers/api/userController.js new file mode 100644 index 00000000..27d80b48 --- /dev/null +++ b/controllers/api/userController.js @@ -0,0 +1,60 @@ +const bcrypt = require('bcryptjs') +const db = require('../../models') +const User = db.User + +// JWT +const jwt = require('jsonwebtoken') +const passportJWT = require('passport-jwt') +const ExtractJwt = passportJWT.ExtractJwt +const JwtStrategy = passportJWT.Strategy + +let userController = { + signIn: (req, res) => { + // 檢查必要資料 + if (!req.body.email || !req.body.password) { + return res.json({ status: 'error', message: "required fields didn't exist" }) + } + // 檢查 user 是否存在與密碼是否正確 + let username = req.body.email + let password = req.body.password + + User.findOne({ where: { email: username } }).then(user => { + if (!user) return res.status(401).json({ status: 'error', message: 'no such user found' }) + if (!bcrypt.compareSync(password, user.password)) { + return res.status(401).json({ status: 'error', message: 'passwords did not match' }) + } + // 簽發 token + var payload = { id: user.id } + var token = jwt.sign(payload, process.env.JWT_SECRET) + return res.json({ + status: 'success', + message: 'ok', + token: token, + user: { + id: user.id, name: user.name, email: user.email, isAdmin: user.isAdmin + } + }) + }) + }, + signUp: (req, res) => { + if(req.body.passwordCheck !== req.body.password){ + return res.json({ status: 'error', message: '兩次密碼輸入不同!'}) + } else { + User.findOne({where: {email: req.body.email}}).then(user => { + if(user){ + return res.json({ status: 'error', message: '信箱重複!'}) + } else { + User.create({ + name: req.body.name, + email: req.body.email, + password: bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10), null) + }).then(user => { + return res.json({ status: 'success', message: '成功註冊帳號!'}) + }) + } + }) + } + } +} + +module.exports = userController \ No newline at end of file diff --git a/controllers/categoryController.js b/controllers/categoryController.js new file mode 100644 index 00000000..eb9290b8 --- /dev/null +++ b/controllers/categoryController.js @@ -0,0 +1,51 @@ +const db = require('../models') +const Category = db.Category +const adminService = require('../services/adminService') + +let categoryController = { + getCategories: (req, res) => { + return Category.findAll({ + raw: true, + nest: true + }).then(categories => { + if (req.params.id) { + Category.findByPk(req.params.id) + .then((category) => { + return res.render('admin/categories', { + categories: categories, + category: category.toJSON() + }) + }) + } else { + return res.render('admin/categories', { categories: categories }) + } + }) + }, + postCategory: (req, res) => { + adminService.postCategory(req, res, data => { + if (data['status'] === 'error') { + req.flash('error_messages', data['message']) + return res.redirect('back') + } + res.redirect('/admin/categories') + }) + }, + putCategory: (req, res) => { + adminService.putCategory(req, res, data => { + if (data['status'] === 'error') { + req.flash('error_messages', data['message']) + return res.redirect('back') + } + res.redirect('/admin/categories') + }) + }, + deleteCategory: (req, res) => { + adminService.deleteCategory(req, res, data => { + if (data['status'] === 'success') { + return res.redirect('/admin/categories') + } + + }) + } +} +module.exports = categoryController \ No newline at end of file diff --git a/controllers/commentController.js b/controllers/commentController.js new file mode 100644 index 00000000..68e412c3 --- /dev/null +++ b/controllers/commentController.js @@ -0,0 +1,26 @@ +const db = require('../models') +const Comment = db.Comment + +const commentController = { + postComment: (req, res) => { + return Comment.create({ + text: req.body.text, + RestaurantId: req.body.restaurantId, + UserId: req.user.id + }) + .then((comment) => { + res.redirect(`/restaurants/${req.body.restaurantId}`) + }) + }, + deleteComment: (req, res) => { + return Comment.findByPk(req.params.id) + .then((comment) => { + comment.destroy() + .then((comment) => { + res.redirect(`/restaurants/${comment.RestaurantId}`) + }) + }) + } +} + +module.exports = commentController \ No newline at end of file diff --git a/controllers/restController.js b/controllers/restController.js new file mode 100644 index 00000000..51077281 --- /dev/null +++ b/controllers/restController.js @@ -0,0 +1,125 @@ +const db = require('../models') +const helper = require('../_helpers') +const Restaurant = db.Restaurant +const Category = db.Category +const Comment = db.Comment +const User = db.User + +const pageLimit = 10 + +const restController = { + getRestaurants: (req, res) => { + let offset = 0 + const whereQuery = {} + let categoryId = '' + if (req.query.page) { + offset = (req.query.page - 1) * pageLimit + } + if (req.query.categoryId) { + categoryId = Number(req.query.categoryId) + whereQuery.categoryId = categoryId + } + Restaurant.findAndCountAll({ + include: Category, + where: whereQuery, + offset: offset, + limit: pageLimit + }).then(result => { + // data for pagination + const page = Number(req.query.page) || 1 + const pages = Math.ceil(result.count / pageLimit) + const totalPage = Array.from({ length: pages }).map((item, index) => index + 1) + const prev = page - 1 < 1 ? 1 : page - 1 + const next = page + 1 > pages ? pages : page + 1 + + // clean up restaurant data + const data = result.rows.map(r => ({ + ...r.dataValues, + description: r.dataValues.description.substring(0, 50), + categoryName: r.dataValues.Category.name, + isFavorited: req.user.FavoritedRestaurants.map(d => d.id).includes(r.id), + isLiked: req.user.LikedRestaurants.map(d => d.id).includes(r.id) + })) + Category.findAll({ + raw: true, + nest: true + }).then(categories => { + return res.render('restaurants', { + restaurants: data, + categories: categories, + categoryId: categoryId, + page: page, + totalPage: totalPage, + prev: prev, + next: next + }) + }) + }) + }, + getRestaurant: (req, res) => { + return Restaurant.findByPk(req.params.id, { + include: [ + Category, + { model: User, as: 'FavoritedUsers' }, + { model: User, as: 'LikedUsers' }, + { model: Comment, include: [User] } + ] + }).then(restaurant =>{ + const isFavorited = restaurant.FavoritedUsers.map(d => d.id).includes(req.user.id) + const isLiked = restaurant.LikedUsers.map(d => d.id).includes(req.user.id) + return restaurant.increment({viewCounts: 1}) + .then(() =>res.render('restaurant', { + restaurant: restaurant.toJSON(), + isFavorited, + isLiked + })) + }) + }, + getFeeds: (req, res) => { + return Promise.all([ + Restaurant.findAll({ + limit: 10, + raw: true, + nest: true, + order: [['createdAt', 'DESC']], + include: [Category] + }), + Comment.findAll({ + limit: 10, + raw: true, + nest: true, + order: [['createdAt', 'DESC']], + include: [User, Restaurant] + }) + ]).then(([restaurants, comments]) => { + return res.render('feeds', { + restaurants: restaurants, + comments: comments + }) + }) + }, + getDashBoard: (req, res) => { + return Restaurant.findByPk(req.params.id, { + include: [Comment, Category] + }) + .then(restaurant => { + return res.render('dashboard', { restaurant: restaurant.toJSON()}) + }) + }, + getTopRestaurant: (req, res) => { + Restaurant.findAll({ + include: [{ model: User, as: 'FavoritedUsers'}], + }) + .then(restaurant => { + const data = restaurant.map(i => ({ + ...i.dataValues, + FavoritedUsersCount: i.dataValues.FavoritedUsers.length, + isFollowed: i.dataValues.FavoritedUsers.map(d => d.id).includes(helper.getUser(req).id) + })).sort((a, b) => b.FavoritedUsers.length - a.FavoritedUsers.length).slice(0, 10) + //console.log(data) + return res.render('topRestaurant', { restaurants: data }) + }) + } +} + +module.exports = restController \ No newline at end of file diff --git a/controllers/userController.js b/controllers/userController.js new file mode 100644 index 00000000..78eaf7cd --- /dev/null +++ b/controllers/userController.js @@ -0,0 +1,198 @@ +const imgur = require('imgur-node-api') +const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID +const bcrypt = require('bcryptjs') +const db = require('../models') +const helper = require('../_helpers') +const User = db.User +const Comment = db.Comment +const Restaurant = db.Restaurant +const Favorite = db.Favorite +const Like = db.Like +const Followship = db.Followship + +const userController = { + signUpPage: (req, res) => { + return res.render('signup') + }, + + signUp: (req, res) => { + // confirm password + if(req.body.passwordCheck !== req.body.password){ + req.flash('error_messages', '兩次密碼輸入不同!') + return res.redirect('/signup') + } else { + // confirm unique user + User.findOne({where: {email: req.body.email}}).then(user => { + if(user){ + req.flash('error_messages', '信箱重複!') + return res.redirect('/signup') + } else { + User.create({ + name: req.body.name, + email: req.body.email, + password: bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10), null) + }).then(user => { + req.flash('success_messages', '成功註冊帳號!') + return res.redirect('/signin') + }) + } + }) + } + }, + + signInPage: (req, res) => { + return res.render('signin') + }, + + signIn: (req, res) => { + req.flash('success_messages', '成功登入!') + res.redirect('/restaurants') + }, + + logout: (req, res) => { + req.flash('success_messages', '登出成功!') + req.logout() + res.redirect('/signin') + }, + + getUser: (req, res) => { + return User.findByPk(req.params.id, { + include: { model: Comment, include: [Restaurant] } + }) + .then(user => res.render('profile', { user: user.toJSON() })) + }, + + editUser: (req, res) => { + return User.findByPk(req.params.id) + .then(user => res.render('edit', { user: user.toJSON() })) + }, + //使用 async/await 改寫失敗 (圖片無法上傳更新到資料庫內) + // putUser: async (req, res) => { + // const userId = req.params.id + // const userData = await User.findByPk(userId) + // const { file } = req + // if (file) { + // imgur.setClientID(IMGUR_CLIENT_ID) + // await imgur.upload(file.path, (err, img) => { + // return userData.update({ image: img.data.link }) + // }) + // } + // await userData.update({ + // name: req.body.name, + // email: req.body.email + // }) + // await req.flash('success_messages','使用者資料編輯成功') + // return res.redirect(`/users/${userId}`) + // } + putUser: (req,res) => { + return User.findByPk(req.params.id) + .then(user =>{ + const { file } = req + if (file) { + imgur.setClientID(IMGUR_CLIENT_ID) + imgur.upload(file.path, (err, img) => { + return user.update({ + name: req.body.name, + email: req.body.email, + image: file ? img.data.link : null + }) + .then(()=>{ + req.flash('success_messages','使用者資料編輯成功') + return res.redirect(`/users/${req.params.id}`) + }) + }) + } else { + return user.update({ + name: req.body.name, + email: req.body.email + }) + .then(()=>{ + req.flash('success_messages','使用者資料編輯成功') + return res.redirect(`/users/${req.params.id}`) + }) + } + }) + }, + addFavorite: (req, res) => { + return Favorite.create({ + UserId: helper.getUser(req).id, + RestaurantId: req.params.restaurantId + }) + .then((restaurant) => { + return res.redirect('back') + }) + }, + removeFavorite: (req, res) => { + return Favorite.findOne({ + where: { + UserId: helper.getUser(req).id, + RestaurantId: req.params.restaurantId + } + }) + .then((favorite) => { + favorite.destroy() + .then((restaurant) => { + return res.redirect('back') + }) + }) + }, + addLike: (req, res) => { + return Like.create({ + UserId: helper.getUser(req).id, + RestaurantId: req.params.restaurantId + }) + .then(() => res.redirect('back')) + }, + removeLike: (req, res) => { + return Like.destroy({ + where: { + UserId: helper.getUser(req).id, + RestaurantId: req.params.restaurantId + } + }) + .then(() => res.redirect('back')) + }, + getTopUser: (req, res) => { + return User.findAll({ + include: [ + { model: User, as: 'Followers' } + ] + }).then(users => { + // 整理 users 資料 + users = users.map(user => ({ + ...user.dataValues, + // 計算追蹤者人數 + FollowerCount: user.Followers.length, + // 判斷目前登入使用者是否已追蹤該 User 物件 + isFollowed: req.user.Followings.map(d => d.id).includes(user.id) + })) + // 依追蹤者人數排序清單 + users = users.sort((a, b) => b.FollowerCount - a.FollowerCount) + return res.render('topUser', { users: users }) + }) + }, + addFollowing: (req, res) => { + return Followship.create({ + followerId: req.user.id, + followingId: req.params.userId + }) + .then((followship) => { + return res.redirect('back') + }) + }, + + removeFollowing: (req, res) => { + return Followship.findOne({where: { + followerId: req.user.id, + followingId: req.params.userId + }}) + .then((followship) => { + followship.destroy() + .then((followship) => { + return res.redirect('back') + }) + }) + } +} + +module.exports = userController \ No newline at end of file diff --git a/helpers/handlebarsHelpers.js b/helpers/handlebarsHelpers.js new file mode 100644 index 00000000..db9edb5f --- /dev/null +++ b/helpers/handlebarsHelpers.js @@ -0,0 +1,21 @@ +const moment = require('moment') + +module.exports = { + showRole: function (isAdmin) { + if (isAdmin) return 'admin' + return 'user' + }, + setRole: function (isAdmin) { + if (isAdmin) return 'set as user' + return 'set as admin' + }, + ifCond: function (a, b, options) { + if (a === b) { + return options.fn(this) + } + return options.inverse(this) + }, + moment: function (a) { + return moment(a).fromNow() + } +} \ No newline at end of file diff --git a/migrations/20211118145538-create-user.js b/migrations/20211118145538-create-user.js new file mode 100644 index 00000000..0554c75c --- /dev/null +++ b/migrations/20211118145538-create-user.js @@ -0,0 +1,36 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Users', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING, + allowNull: false + }, + email: { + type: Sequelize.STRING, + allowNull: false + }, + password: { + type: Sequelize.STRING, + allowNull: false + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Users'); + } +}; \ No newline at end of file diff --git a/migrations/20211119014024-add-isAdmin-to-users.js b/migrations/20211119014024-add-isAdmin-to-users.js new file mode 100644 index 00000000..60e6fe08 --- /dev/null +++ b/migrations/20211119014024-add-isAdmin-to-users.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Users', 'isAdmin', { + type: Sequelize.BOOLEAN, + defaultValue: false, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Users', 'isAdmin'); + } +}; diff --git a/migrations/20211119021226-create-restaurant.js b/migrations/20211119021226-create-restaurant.js new file mode 100644 index 00000000..9ae279d5 --- /dev/null +++ b/migrations/20211119021226-create-restaurant.js @@ -0,0 +1,39 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Restaurants', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING + }, + tel: { + type: Sequelize.STRING + }, + address: { + type: Sequelize.STRING + }, + opening_hours: { + type: Sequelize.STRING + }, + description: { + type: Sequelize.TEXT + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Restaurants'); + } +}; \ No newline at end of file diff --git a/migrations/20211119052746-add-image-to-restaurants.js b/migrations/20211119052746-add-image-to-restaurants.js new file mode 100644 index 00000000..d6c12e1d --- /dev/null +++ b/migrations/20211119052746-add-image-to-restaurants.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn( 'Restaurants', 'image', { + type: Sequelize.STRING + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn( 'Restaurants', 'image' ); + } +}; diff --git a/migrations/20211122161412-create-category.js b/migrations/20211122161412-create-category.js new file mode 100644 index 00000000..ab0ee89a --- /dev/null +++ b/migrations/20211122161412-create-category.js @@ -0,0 +1,27 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Categories', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Categories'); + } +}; \ No newline at end of file diff --git a/migrations/20211122161458-add-categoryId-to-restaurant.js b/migrations/20211122161458-add-categoryId-to-restaurant.js new file mode 100644 index 00000000..661f8700 --- /dev/null +++ b/migrations/20211122161458-add-categoryId-to-restaurant.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Restaurants', 'CategoryId', { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'Categories', + key: 'id' + } + }) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Restaurants', 'CategoryId') + } +}; diff --git a/migrations/20211124144738-create-comment.js b/migrations/20211124144738-create-comment.js new file mode 100644 index 00000000..d8d096f6 --- /dev/null +++ b/migrations/20211124144738-create-comment.js @@ -0,0 +1,35 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Comments', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + text: { + type: Sequelize.STRING + }, + UserId: { + type: Sequelize.INTEGER, + references: { model: 'Users', key: 'id' } + }, + RestaurantId: { + type: Sequelize.INTEGER, + references: { model: 'Restaurants', key: 'id' } + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Comments'); + } +}; \ No newline at end of file diff --git a/migrations/20211124162917-add-image-to-users.js b/migrations/20211124162917-add-image-to-users.js new file mode 100644 index 00000000..d469cffd --- /dev/null +++ b/migrations/20211124162917-add-image-to-users.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn( 'Users', 'image', { + type: Sequelize.STRING, + defaultValue: 'https://i.epochtimes.com/assets/uploads/2021/08/id13156667-shutterstock_376153318-450x322.jpg' + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn( 'Users', 'image' ); + } +}; \ No newline at end of file diff --git a/migrations/20211127150828-add-viewCounts-to-restaurants.js b/migrations/20211127150828-add-viewCounts-to-restaurants.js new file mode 100644 index 00000000..ed49e221 --- /dev/null +++ b/migrations/20211127150828-add-viewCounts-to-restaurants.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn( 'Restaurants', 'viewCounts', { + type: Sequelize.INTEGER, + defaultValue: 0 + }) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn( 'Restaurants', 'viewCounts' ); + } +}; diff --git a/migrations/20211127161400-create-favorite.js b/migrations/20211127161400-create-favorite.js new file mode 100644 index 00000000..6b9a5748 --- /dev/null +++ b/migrations/20211127161400-create-favorite.js @@ -0,0 +1,30 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Favorites', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + UserId: { + type: Sequelize.INTEGER + }, + RestaurantId: { + type: Sequelize.INTEGER + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Favorites'); + } +}; \ No newline at end of file diff --git a/migrations/20211210053940-create-like.js b/migrations/20211210053940-create-like.js new file mode 100644 index 00000000..ad2df697 --- /dev/null +++ b/migrations/20211210053940-create-like.js @@ -0,0 +1,30 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Likes', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + UserId: { + type: Sequelize.INTEGER + }, + RestaurantId: { + type: Sequelize.INTEGER + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Likes'); + } +}; \ No newline at end of file diff --git a/migrations/20211211160647-create-followship.js b/migrations/20211211160647-create-followship.js new file mode 100644 index 00000000..a2905e94 --- /dev/null +++ b/migrations/20211211160647-create-followship.js @@ -0,0 +1,30 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Followships', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + followerId: { + type: Sequelize.INTEGER + }, + followingId: { + type: Sequelize.INTEGER + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Followships'); + } +}; \ No newline at end of file diff --git a/models/category.js b/models/category.js new file mode 100644 index 00000000..90b0c491 --- /dev/null +++ b/models/category.js @@ -0,0 +1,23 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Category extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + Category.hasMany(models.Restaurant) + } + }; + Category.init({ + name: DataTypes.STRING + }, { + sequelize, + modelName: 'Category', + }); + return Category; +}; \ No newline at end of file diff --git a/models/comment.js b/models/comment.js new file mode 100644 index 00000000..41b4ca37 --- /dev/null +++ b/models/comment.js @@ -0,0 +1,26 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Comment extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + Comment.belongsTo(models.Restaurant) + Comment.belongsTo(models.User) + } + }; + Comment.init({ + text: DataTypes.STRING, + UserId: DataTypes.INTEGER, + RestaurantId: DataTypes.INTEGER + }, { + sequelize, + modelName: 'Comment', + }); + return Comment; +}; \ No newline at end of file diff --git a/models/favorite.js b/models/favorite.js new file mode 100644 index 00000000..3def4705 --- /dev/null +++ b/models/favorite.js @@ -0,0 +1,24 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Favorite extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + }; + Favorite.init({ + UserId: DataTypes.INTEGER, + RestaurantId: DataTypes.INTEGER + }, { + sequelize, + modelName: 'Favorite', + }); + return Favorite; +}; \ No newline at end of file diff --git a/models/followship.js b/models/followship.js new file mode 100644 index 00000000..bb4a9c3c --- /dev/null +++ b/models/followship.js @@ -0,0 +1,23 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Followship extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + } + }; + Followship.init({ + followerId: DataTypes.INTEGER, + followingId: DataTypes.INTEGER + }, { + sequelize, + modelName: 'Followship', + }); + return Followship; +}; \ No newline at end of file diff --git a/models/index.js b/models/index.js index 3d701d98..a28ab951 100644 --- a/models/index.js +++ b/models/index.js @@ -28,8 +28,8 @@ fs.readdirSync(__dirname) file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' ) }) - .forEach((file) => { - const model = sequelize['import'](path.join(__dirname, file)) + .forEach(file => { + const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes) db[model.name] = model }) diff --git a/models/like.js b/models/like.js new file mode 100644 index 00000000..b45c4dde --- /dev/null +++ b/models/like.js @@ -0,0 +1,24 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Like extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + }; + Like.init({ + UserId: DataTypes.INTEGER, + RestaurantId: DataTypes.INTEGER + }, { + sequelize, + modelName: 'Like', + }); + return Like; +}; \ No newline at end of file diff --git a/models/restaurant.js b/models/restaurant.js new file mode 100644 index 00000000..0ff4d1e9 --- /dev/null +++ b/models/restaurant.js @@ -0,0 +1,40 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Restaurant extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + Restaurant.belongsTo(models.Category) + Restaurant.hasMany(models.Comment) + Restaurant.belongsToMany(models.User, { + through: models.Favorite, + foreignKey: 'RestaurantId', + as: 'FavoritedUsers' + }) + Restaurant.belongsToMany(models.User, { + through: models.Like, + foreignKey: 'RestaurantId', + as: 'LikedUsers' + }) + } + }; + Restaurant.init({ + name: DataTypes.STRING, + tel: DataTypes.STRING, + address: DataTypes.STRING, + opening_hours: DataTypes.STRING, + description: DataTypes.TEXT, + image: DataTypes.STRING, + viewCounts: DataTypes.INTEGER + }, { + sequelize, + modelName: 'Restaurant', + }); + return Restaurant; +}; \ No newline at end of file diff --git a/models/user.js b/models/user.js new file mode 100644 index 00000000..dcbc482e --- /dev/null +++ b/models/user.js @@ -0,0 +1,47 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class User extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + User.hasMany(models.Comment) + User.belongsToMany(models.Restaurant, { + through: models.Favorite, + foreignKey: 'UserId', + as: 'FavoritedRestaurants' + }) + User.belongsToMany(models.Restaurant, { + through: models.Like, + foreignKey: 'UserId', + as: 'LikedRestaurants' + }) + User.belongsToMany(User, { + through: models.Followship, + foreignKey: 'followingId', + as: 'Followers' + }) + User.belongsToMany(User, { + through: models.Followship, + foreignKey: 'followerId', + as: 'Followings' + }) + } + }; + User.init({ + name: DataTypes.STRING, + email: DataTypes.STRING, + password: DataTypes.STRING, + isAdmin: DataTypes.BOOLEAN, + image: DataTypes.STRING + }, { + sequelize, + modelName: 'User', + }); + return User; +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bb00f848..723ef604 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,19 @@ { - "name": "forum-express-workspace", - "version": "1.0.0", + "name": "forum-express-grading", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, "@sinonjs/commons": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, "requires": { "type-detect": "4.0.8" } @@ -16,6 +22,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" } @@ -24,6 +31,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, "requires": { "@sinonjs/commons": "^1", "@sinonjs/samsam": "^5.0.2" @@ -33,6 +41,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.2.0.tgz", "integrity": "sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw==", + "dev": true, "requires": { "@sinonjs/commons": "^1.6.0", "lodash.get": "^4.4.2", @@ -42,7 +51,16 @@ "@sinonjs/text-encoding": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } }, "@types/node": { "version": "14.14.5", @@ -68,6 +86,49 @@ "negotiator": "0.6.2" } }, + "ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "requires": { + "string-width": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -100,6 +161,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -118,16 +184,32 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" }, + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/aws-sign/-/aws-sign-0.2.0.tgz", + "integrity": "sha1-xVAThWyBlOyFSgy+yQqrWgTOOsU=" }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, "binary-extensions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", @@ -155,6 +237,100 @@ "type-is": "~1.6.17" } }, + "boom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/boom/-/boom-0.3.8.tgz", + "integrity": "sha1-yM2wQUNZEnQWKMBE7Mcy0dF8Ceo=", + "requires": { + "hoek": "0.7.x" + } + }, + "boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -177,11 +353,82 @@ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -252,6 +499,16 @@ "readdirp": "~3.5.0" } }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + }, "cli-color": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz", @@ -275,6 +532,14 @@ "wrap-ansi": "^5.1.0" } }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -292,6 +557,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -304,13 +570,54 @@ "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "config-chain": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", @@ -320,6 +627,24 @@ "proto-list": "~1.2.1" } }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "connect-flash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", + "integrity": "sha1-2GMPJtlaf4UfmVax6MxnMvO2qjA=" + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -338,6 +663,11 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, + "cookie-jar": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cookie-jar/-/cookie-jar-0.2.0.tgz", + "integrity": "sha1-ZOzAasl423leS1KQy+SLo3gUAPo=" + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -346,7 +676,26 @@ "cookiejar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cryptiles": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.1.3.tgz", + "integrity": "sha1-GlVnNPBtJLo0hirpy55wmjr7/xw=", + "requires": { + "boom": "0.3.x" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" }, "d": { "version": "1.0.1", @@ -370,6 +719,14 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -378,10 +735,21 @@ "type-detect": "^4.0.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true }, "denque": { "version": "1.4.1", @@ -398,16 +766,69 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + }, "dottie": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", @@ -455,6 +876,14 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "es5-ext": { "version": "0.10.53", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", @@ -495,6 +924,11 @@ "es6-symbol": "^3.1.1" } }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -561,6 +995,68 @@ "vary": "~1.1.2" } }, + "express-handlebars": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.3.3.tgz", + "integrity": "sha512-/tWy/VmfdaZ5RUZUprgDzcCTj4QEnaXHbtLSXr4ROTpC/3aynrg4nIkn1+eLlcEp4Cs7HkxDlu2jJaz3aBG//w==", + "requires": { + "glob": "^7.1.7", + "graceful-fs": "^4.2.7", + "handlebars": "^4.7.7" + }, + "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + } + } + }, + "express-session": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz", + "integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "ext": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", @@ -576,10 +1072,26 @@ } } }, + "faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==" + }, "fast-safe-stringify": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", + "dev": true + }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true, + "requires": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + } }, "fill-range": { "version": "7.0.1", @@ -616,10 +1128,16 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" }, + "forever-agent": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.2.0.tgz", + "integrity": "sha1-4cJcetROCcOPIzh2x2/MJP+EOx8=" + }, "form-data": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -629,7 +1147,8 @@ "formidable": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "dev": true }, "forwarded": { "version": "0.1.2", @@ -685,6 +1204,14 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -706,6 +1233,39 @@ "is-glob": "^4.0.1" } }, + "global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "requires": { + "ini": "2.0.0" + }, + "dependencies": { + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==" + } + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -716,6 +1276,18 @@ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -729,11 +1301,37 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, + "hawk": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-0.10.2.tgz", + "integrity": "sha1-mzYd7pWpMWQObVBOBWCaj8OsRdI=", + "requires": { + "boom": "0.3.x", + "cryptiles": "0.1.x", + "hoek": "0.7.x", + "sntp": "0.1.x" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "hoek": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.7.6.tgz", + "integrity": "sha1-YPvZBFV1Qc0rh5Wr8wihs3cOFVo=" + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -754,6 +1352,29 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" + }, + "imgur-node-api": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/imgur-node-api/-/imgur-node-api-0.1.0.tgz", + "integrity": "sha1-iJU25/x9/FyYXHtVeMGuwxyzwYY=", + "requires": { + "request": "~2.16.6" + } + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, "inflection": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", @@ -791,6 +1412,14 @@ "binary-extensions": "^2.0.0" } }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, "is-core-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.0.0.tgz", @@ -817,11 +1446,41 @@ "is-extglob": "^2.1.1" } }, + "is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "requires": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + } + }, + "is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==" + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -837,6 +1496,16 @@ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -868,6 +1537,16 @@ "esprima": "^4.0.0" } }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, + "json-stringify-safe": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-3.0.0.tgz", + "integrity": "sha1-nbew5TDH8onF6MhDKvGRwv91pbM=" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -876,14 +1555,79 @@ "graceful-fs": "^4.1.6" } }, - "just-extend": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", - "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==" - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "requires": { + "package-json": "^6.3.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "requires": { "p-locate": "^3.0.0", @@ -898,7 +1642,43 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, "log-symbols": { "version": "4.0.0", @@ -913,6 +1693,11 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -929,6 +1714,21 @@ "es5-ext": "~0.10.2" } }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -954,6 +1754,27 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "requires": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -977,6 +1798,11 @@ "mime-db": "1.44.0" } }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -985,6 +1811,11 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -1075,6 +1906,12 @@ } } }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, "moment": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", @@ -1093,6 +1930,31 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", + "integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + } + } + }, "mysql2": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz", @@ -1152,6 +2014,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -1161,6 +2028,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "dev": true, "requires": { "@sinonjs/commons": "^1.7.0", "@sinonjs/fake-timers": "^6.0.0", @@ -1173,12 +2041,109 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, "requires": { "isarray": "0.0.1" } } } }, + "nodemon": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz", + "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==", + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5", + "update-notifier": "^5.1.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -1192,6 +2157,21 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, + "normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" + }, + "oauth-sign": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.2.0.tgz", + "integrity": "sha1-oOahcV2u0GLzIrYit/5a/RA1tuI=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -1200,6 +2180,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1208,6 +2193,11 @@ "wrappy": "1" } }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -1229,11 +2219,65 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.5.0.tgz", + "integrity": "sha512-ln+ue5YaNDS+fes6O5PCzXKSseY5u8MYhX9H5Co4s+HfYI5oqvnHKoOORLYDUPh+8tHvrxugF2GFcUA1Q1Gqfg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "requires": { + "jsonwebtoken": "^8.2.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -1259,11 +2303,103 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, + "pg": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz", + "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.4.1", + "pg-protocol": "^1.5.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-connection-string": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-pool": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", + "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==" + }, + "pg-protocol": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", + "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==", + "requires": { + "split2": "^3.1.1" + } + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -1278,16 +2414,54 @@ "ipaddr.js": "1.9.1" } }, + "proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "requires": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "requires": { + "escape-goat": "^2.0.0" + } + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1312,6 +2486,24 @@ "unpipe": "1.0.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + } + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -1330,6 +2522,80 @@ "picomatch": "^2.2.1" } }, + "registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "requires": { + "rc": "^1.2.8" + } + }, + "request": { + "version": "2.16.6", + "resolved": "https://registry.npmjs.org/request/-/request-2.16.6.tgz", + "integrity": "sha1-hy/kRa5y3iZrN4edatfclI+gHK0=", + "requires": { + "aws-sign": "~0.2.0", + "cookie-jar": "~0.2.0", + "forever-agent": "~0.2.0", + "form-data": "~0.0.3", + "hawk": "~0.10.2", + "json-stringify-safe": "~3.0.0", + "mime": "~1.2.7", + "node-uuid": "~1.4.0", + "oauth-sign": "~0.2.0", + "qs": "~0.5.4", + "tunnel-agent": "~0.2.0" + }, + "dependencies": { + "combined-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", + "requires": { + "delayed-stream": "0.0.5" + } + }, + "delayed-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=" + }, + "form-data": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.0.10.tgz", + "integrity": "sha1-2zRaU3jYau6x7V1VO4aawZLS9e0=", + "requires": { + "async": "~0.2.7", + "combined-stream": "~0.0.4", + "mime": "~1.2.2" + } + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + }, + "qs": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz", + "integrity": "sha1-MbGtBYVnZRxSaSFQa5qHk5EaA4Q=" + } + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1349,6 +2615,14 @@ "path-parse": "^1.0.6" } }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "retry-as-promised": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz", @@ -1372,6 +2646,21 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, "send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", @@ -1453,6 +2742,17 @@ "yargs": "^13.1.0" } }, + "sequelize-mock": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/sequelize-mock/-/sequelize-mock-0.10.2.tgz", + "integrity": "sha1-GdOXHM2utbhkFwwkznkqinHxRL0=", + "dev": true, + "requires": { + "bluebird": "^3.4.6", + "inflection": "^1.10.0", + "lodash": "^4.16.4" + } + }, "sequelize-pool": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-6.1.0.tgz", @@ -1492,10 +2792,16 @@ "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" }, + "signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, "sinon": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.0.tgz", "integrity": "sha512-eSNXz1XMcGEMHw08NJXSyTHIu6qTCOiN8x9ODACmZpNQpr0aXTBXBnI4xTzQzR+TEpOmLiKowGf9flCuKIzsbw==", + "dev": true, "requires": { "@sinonjs/commons": "^1.8.1", "@sinonjs/fake-timers": "^6.0.1", @@ -1506,6 +2812,27 @@ "supports-color": "^7.1.0" } }, + "sntp": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.1.4.tgz", + "integrity": "sha1-XvSBuVGnspr/30r9fyaDj8ESD4Q=", + "requires": { + "hoek": "0.7.x" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "requires": { + "readable-stream": "^3.0.0" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -1521,6 +2848,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -1570,6 +2902,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", + "dev": true, "requires": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.2", @@ -1588,6 +2921,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -1595,17 +2929,20 @@ "mime": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "dev": true }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "qs": { "version": "6.9.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", + "dev": true } } }, @@ -1613,6 +2950,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/supertest/-/supertest-5.0.0.tgz", "integrity": "sha512-2JAWpPrUOZF4hHH5ZTCN2xjKXvJS3AEwPNXl0HUseHsfcXFvMy9kcsufIHCNAmQ5hlGCvgeAqaR5PBEouN3hlQ==", + "dev": true, "requires": { "methods": "1.1.2", "superagent": "6.1.0" @@ -1635,6 +2973,11 @@ "next-tick": "1" } }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1653,6 +2996,29 @@ "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "requires": { + "abbrev": "1" + } + } + } + }, + "tunnel-agent": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.2.0.tgz", + "integrity": "sha1-aFPCr7GyEJ5FYp5JK9419Fnqaeg=" + }, "type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -1663,6 +3029,11 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1672,6 +3043,33 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "uglify-js": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.3.tgz", + "integrity": "sha512-mic3aOdiq01DuSVx0TseaEzMIVqebMZ0Z3vaeDhFEh9bsc24hV1TFvN74reA2vs08D0ZWfNjAcJ3UbVLaBss+g==", + "optional": true + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", @@ -1680,6 +3078,19 @@ "bluebird": "^3.7.2" } }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -1690,6 +3101,45 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "update-notifier": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", + "requires": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.1.0", + "pupa": "^2.1.1", + "semver": "^7.3.4", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -1760,6 +3210,49 @@ } } }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "requires": { + "string-width": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "wkx": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", @@ -1768,6 +3261,11 @@ "@types/node": "*" } }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "workerpool": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.2.tgz", @@ -1788,6 +3286,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", @@ -1848,4 +3367,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 5f6f1d0f..d744bad3 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,27 @@ "url": "git+https://github.com/ALPHACamp/forum-express-grading.git" }, "dependencies": { + "bcryptjs": "^2.4.3", + "body-parser": "^1.19.0", + "chai": "^4.2.0", + "connect-flash": "^0.1.1", + "dotenv": "^10.0.0", "express": "^4.17.1", + "express-handlebars": "^5.3.3", + "express-session": "^1.17.2", + "faker": "^5.5.3", + "imgur-node-api": "^0.1.0", + "jsonwebtoken": "^8.5.1", + "method-override": "^3.0.0", + "mocha": "^8.2.0", + "moment": "^2.29.1", + "multer": "^1.4.3", "mysql2": "^2.2.5", "nodemon": "^2.0.12", + "passport": "^0.5.0", + "passport-jwt": "^4.0.0", + "passport-local": "^1.0.0", + "pg": "^8.7.1", "sequelize": "^6.3.5", "sequelize-cli": "^6.2.0" }, @@ -26,4 +44,4 @@ "sinon": "^9.2.0", "supertest": "^5.0.0" } -} \ No newline at end of file +} diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 00000000..517d475f --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,8 @@ +.ellipsis { + overflow: hidden; + display: -webkit-box; + text-overflow: ellipsis; + -webkit-line-clamp: 2; /*行數*/ + -webkit-box-orient: vertical; + white-space: normal; +} diff --git a/routes/apis.js b/routes/apis.js new file mode 100644 index 00000000..10ecb918 --- /dev/null +++ b/routes/apis.js @@ -0,0 +1,32 @@ +const express = require('express') +const router = express.Router() +const adminController = require('../controllers/api/adminController.js') +const userController = require('../controllers/api/userController.js') +const multer = require('multer') +const upload = multer({ dest: 'temp/' }) +const passport = require('../config/passport') + +const authenticated = passport.authenticate('jwt', { session: false }) + +const authenticatedAdmin = (req, res, next) => { + if (req.user) { + if (req.user.isAdmin) { return next() } + return res.json({ status: 'error', message: 'permission denied' }) + } else { + return res.json({ status: 'error', message: 'permission denied' }) + } +} + +router.get('/admin/restaurants', authenticated, authenticatedAdmin, adminController.getRestaurants) +router.get('/admin/restaurants/:id', adminController.getRestaurant) +router.get('/admin/categories', adminController.getCategories) +router.delete('/admin/restaurants/:id', adminController.deleteRestaurant) +router.post('/admin/restaurants', upload.single('image'), adminController.postRestaurant) +router.put('/admin/restaurants/:id', upload.single('image'), adminController.putRestaurant) +router.post('/admin/categories', adminController.postCategory) +router.put('/admin/categories/:id', adminController.putCategory) +router.delete('/admin/categories/:id', adminController.deleteCategory) +router.post('/signin', userController.signIn) +router.post('/signup', userController.signUp) + +module.exports = router \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index b20f151b..112e5fd6 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,5 +1,9 @@ -module.exports = (app) => { - app.get('/', (req, res) => { - res.send('Hello World!') - }) +const routes = require('./routes') +const apis = require('./apis') + +module.exports = (router) => { + router.use('/api', apis) + router.use('/', routes) } + + diff --git a/routes/routes.js b/routes/routes.js new file mode 100644 index 00000000..890f2972 --- /dev/null +++ b/routes/routes.js @@ -0,0 +1,82 @@ +const express = require('express') +const router = express.Router() +const passport = require('../config/passport') +const restController = require('../controllers/restController') +const adminController = require('../controllers/adminController.js') +const userController = require('../controllers/userController.js') +const categoryController = require('../controllers/categoryController.js') +const commentController = require('../controllers/commentController.js') +const helpers = require('../_helpers') +const multer = require('multer') +const upload = multer({ dest: 'temp/' }) + +const authenticated = (req, res, next) => { + if (helpers.ensureAuthenticated(req)) { + return next() + } + res.redirect('/signin') +} +const authenticatedAdmin = (req, res, next) => { + if (helpers.ensureAuthenticated(req)) { + if (helpers.getUser(req).isAdmin) { return next() } + return res.redirect('/') + } + res.redirect('/signin') +} + + +//瀏覽首頁、餐廳資料 +router.get('/', authenticated, (req, res) => res.redirect('/restaurants')) +router.get('/restaurants', authenticated,restController.getRestaurants) +router.get('/restaurants/feeds', authenticated,restController.getFeeds) +router.get('/restaurants/top', authenticated,restController.getTopRestaurant) +router.get('/restaurants/:id', authenticated, restController.getRestaurant) +router.get('/restaurants/:id/dashboard', authenticated, restController.getDashBoard) +//瀏覽後台 +router.get('/admin', authenticatedAdmin,(req, res) => res.redirect('/admin/restaurants')) +router.get('/admin/restaurants', authenticatedAdmin, adminController.getRestaurants) +//新增餐廳資料 +router.get('/admin/restaurants/create', authenticatedAdmin, adminController.createRestaurant) +router.post('/admin/restaurants', authenticatedAdmin, upload.single('image'), adminController.postRestaurant) +//瀏覽單一餐廳 +router.get('/admin/restaurants/:id', authenticatedAdmin, adminController.getRestaurant) +//編輯餐廳資料 +router.get('/admin/restaurants/:id/edit', authenticatedAdmin, adminController.editRestaurant) +router.put('/admin/restaurants/:id', authenticatedAdmin, upload.single('image'), adminController.putRestaurant) +//刪除餐廳資料 +router.delete('/admin/restaurants/:id', authenticatedAdmin, adminController.deleteRestaurant) +//顯示使用者清單 +router.get('/admin/users', authenticatedAdmin, adminController.getUsers) +//更改使用者權限 +router.put('/admin/users/:id/toggleAdmin', authenticatedAdmin, adminController.toggleAdmin) +//註冊、登入、登出 +router.get('/signup', userController.signUpPage) +router.post('/signup', userController.signUp) +router.get('/signin', userController.signInPage) +router.post('/signin', passport.authenticate('local', { failureRedirect: '/signin', failureFlash: true }), userController.signIn) +router.get('/logout', userController.logout) +//使用者個人資料 +router.get('/users/top', authenticated, userController.getTopUser) +router.get('/users/:id', authenticated, userController.getUser) +router.get('/users/:id/edit', authenticated, userController.editUser) +router.put('/users/:id', authenticated, upload.single('image'), userController.putUser) +//餐廳分類CRUD +router.get('/admin/categories', authenticatedAdmin, categoryController.getCategories) +router.post('/admin/categories', authenticatedAdmin, categoryController.postCategory) +router.get('/admin/categories/:id', authenticatedAdmin, categoryController.getCategories) +router.put('/admin/categories/:id', authenticatedAdmin, categoryController.putCategory) +router.delete('/admin/categories/:id', authenticatedAdmin, categoryController.deleteCategory) +//評論相關 +router.post('/comments', authenticated, commentController.postComment) +router.delete('/comments/:id', authenticatedAdmin, commentController.deleteComment) +//新增、移除我的最愛 +router.post('/favorite/:restaurantId', authenticated, userController.addFavorite) +router.delete('/favorite/:restaurantId', authenticated, userController.removeFavorite) +//Like / Unlike +router.post('/like/:restaurantId', authenticated, userController.addLike) +router.delete('/like/:restaurantId', authenticated, userController.removeLike) +//使用者following +router.post('/following/:userId', authenticated, userController.addFollowing) +router.delete('/following/:userId', authenticated, userController.removeFollowing) + +module.exports = router diff --git a/seeders/20211119151608-users-seed-file.js b/seeders/20211119151608-users-seed-file.js new file mode 100644 index 00000000..41bdde72 --- /dev/null +++ b/seeders/20211119151608-users-seed-file.js @@ -0,0 +1,33 @@ +'use strict'; +const bcrypt = require('bcryptjs') + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Users', [{ + email: 'root@example.com', + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + isAdmin: true, + name: 'root', + createdAt: new Date(), + updatedAt: new Date() + }, { + email: 'user1@example.com', + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + isAdmin: false, + name: 'user1', + createdAt: new Date(), + updatedAt: new Date() + }, { + email: 'user2@example.com', + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + isAdmin: false, + name: 'user2', + createdAt: new Date(), + updatedAt: new Date() + }], {}) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Users', null, {}) + } +}; diff --git a/seeders/20211123021738-categories-seed-file.js b/seeders/20211123021738-categories-seed-file.js new file mode 100644 index 00000000..a393ef24 --- /dev/null +++ b/seeders/20211123021738-categories-seed-file.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Categories', + ['中式料理', '日本料理', '義大利料理', '墨西哥料理', '素食料理', '美式料理', '複合式料理'] + .map((item, index) => + ({ + id: index * 10 + 1, + name: item, + createdAt: new Date(), + updatedAt: new Date() + }) + ), {}) + }, + + down: async (queryInterface, Sequelize) => { + queryInterface.bulkDelete('Categories', null, {}) + } +}; diff --git a/seeders/20211123022458-restaurants-seed-file.js b/seeders/20211123022458-restaurants-seed-file.js new file mode 100644 index 00000000..5d027673 --- /dev/null +++ b/seeders/20211123022458-restaurants-seed-file.js @@ -0,0 +1,25 @@ +'use strict'; +const faker = require('faker') + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Restaurants', + Array.from({ length: 50 }).map((d, i) => + ({ + name: faker.name.findName(), + tel: faker.phone.phoneNumber(), + address: faker.address.streetAddress(), + opening_hours: '08:00', + image: `https://loremflickr.com/320/240/restaurant,food/?random=${Math.random() * 100}`, + description: faker.lorem.text(), + createdAt: new Date(), + updatedAt: new Date(), + CategoryId: Math.floor(Math.random() * 6) * 10 + 1 + }) + ), {}) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Restaurants', null, {}) + } +}; diff --git a/services/adminService.js b/services/adminService.js new file mode 100644 index 00000000..63f11577 --- /dev/null +++ b/services/adminService.js @@ -0,0 +1,152 @@ +const imgur = require('imgur-node-api') +const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID +const db = require('../models') +const Restaurant = db.Restaurant +const Category = db.Category + +const adminService = { + getRestaurants: (req, res, callback) => { + return Restaurant.findAll({ raw: true, nest: true, include: [Category] }).then(restaurants => { + callback({ restaurants: restaurants }) + }) + }, + getRestaurant: (req, res, callback) => { + return Restaurant.findByPk(req.params.id, {include: [Category]}) + .then(restaurant => { + callback({ restaurant: restaurant.toJSON() }) + }) + }, + getCategories: (req, res, callback) => { + return Category.findAll({ + raw: true, + nest: true + }).then(categories => { + callback({ categories: categories }) + }) + }, + deleteRestaurant: (req, res, callback) => { + return Restaurant.findByPk(req.params.id) + .then((restaurant) => { + restaurant.destroy() + .then(() => { + callback({ status: 'success', message: '' }) + }) + }) + }, + postRestaurant: (req, res, callback) => { + if(!req.body.name){ + callback({status: 'error', message: "name didn't exist"}) + + } + const { file } = req + if (file) { + imgur.setClientID(IMGUR_CLIENT_ID); + imgur.upload(file.path, (err, img) => { + return Restaurant.create({ + name: req.body.name, + tel: req.body.tel, + address: req.body.address, + opening_hours: req.body.opening_hours, + description: req.body.description, + image: file ? img.data.link : null, + CategoryId: req.body.categoryId + }).then((restaurant) => { + callback({ status: 'success', message: 'restaurant was successfully created' }) + }) + }) + } + else { + return Restaurant.create({ + name: req.body.name, + tel: req.body.tel, + address: req.body.address, + opening_hours: req.body.opening_hours, + description: req.body.description, + image: null, + CategoryId: req.body.categoryId + }).then((restaurant) => { + callback({ status: 'success', message: 'restaurant was successfully created' }) + }) + } + }, + putRestaurant: (req, res, callback) => { + if(!req.body.name){ + callback({status: 'error', message: "name didn't exist"}) + } + + const { file } = req + if (file) { + imgur.setClientID(IMGUR_CLIENT_ID); + imgur.upload(file.path, (err, img) => { + return Restaurant.findByPk(req.params.id) + .then((restaurant) => { + restaurant.update({ + name: req.body.name, + tel: req.body.tel, + address: req.body.address, + opening_hours: req.body.opening_hours, + description: req.body.description, + image: file ? img.data.link : restaurant.image, + CategoryId: req.body.categoryId + }) + .then((restaurant) => { + callback({status: 'success', message: 'restaurant was successfully to update'}) + }) + }) + }) + } + else { + return Restaurant.findByPk(req.params.id) + .then((restaurant) => { + restaurant.update({ + name: req.body.name, + tel: req.body.tel, + address: req.body.address, + opening_hours: req.body.opening_hours, + description: req.body.description, + image: restaurant.image + }) + .then((restaurant) => { + callback({status: 'success', message: 'restaurant was successfully to update'}) + }) + }) + } + }, + postCategory: (req, res, callback) => { + if (!req.body.name) { + callback({ status :'error', message: 'name didn\'t exist'}) + } else { + return Category.create({ + name: req.body.name + }) + .then((category) => { + callback({ status :'success', message: ''}) + }) + } + }, + putCategory: (req, res, callback) => { + if (!req.body.name) { + callback({ status :'error', message: 'name didn\'t exist'}) + } else { + return Category.findByPk(req.params.id) + .then((category) => { + category.update(req.body) + .then((category) => { + callback({ status :'success', message: ''}) + }) + }) + } + }, + deleteCategory: (req, res, callback) => { + return Category.findByPk(req.params.id) + .then((category) => { + category.destroy() + .then((category) => { + callback({ status :'success', message: ''}) + }) + }) + } +} + +module.exports = adminService + diff --git a/tests/R02.test.js b/tests/R02.test.js new file mode 100644 index 00000000..26c927b3 --- /dev/null +++ b/tests/R02.test.js @@ -0,0 +1,154 @@ +const chai = require('chai') +const request = require('supertest') +const sinon = require('sinon') +const should = chai.should() + +const helpers = require('../_helpers'); + +const { createModelMock, createControllerProxy, mockRequest, mockResponse } = require('../helpers/unitTestHelpers'); + +describe('# R02', () => { + describe('# R02: 建立 User Profile', function () { + context('# [瀏覽 Profile]', () => { + // 前置準備 + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', { + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }) + + // 修改 userController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table + this.userController = createControllerProxy('../controllers/userController', { User: this.UserMock }) + }) + + // 開始測試 + it(' GET /users/:id ', async () => { + // 模擬 request & response + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /users/1 發出請求 + const res = mockResponse() + + // 測試作業指定的 userController.getUser 函式 + await this.userController.getUser(req, res) + + // toggleAdmin 執行完畢後,應呼叫 res.render + // res.render 的第 1 個參數要是 'profile' + // res.render 的第 2 個參數要是 user,其 name 屬性的值應是 'admin' + res.render.getCall(0).args[0].should.equal('profile') + res.render.getCall(0).args[1].user.name.should.equal('admin') + }) + + // 測試完畢,清除資料 + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context('# [瀏覽編輯 Profile 頁面]', () => { + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', { + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }) + + // 連向模擬的 User table + this.userController = createControllerProxy('../controllers/userController', { User: this.UserMock }) + }) + + it(' GET /users/:id/edit ', async () => { + // 模擬 request & response + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /users/1/edit 發出請求 + const res = mockResponse() + + // 測試作業指定的 adminController.editUser 函式 + await this.userController.editUser(req, res) + + // editUser 執行完畢後,應呼叫 res.render + // res.render 的第 1 個參數要是 'edit' + // res.render 的第 2 個參數要是 user,其 name 屬性的值應是 'admin' + res.render.getCall(0).args[0].should.equal('edit') + res.render.getCall(0).args[1].user.name.should.equal('admin') + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context('# [編輯 Profile]', () => { + before(async () => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + { + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + } + ) + + // 連向模擬的 User table + this.userController = createControllerProxy('../controllers/userController', { User: this.UserMock }) + }) + + it(' PUT /users/:id ', async () => { + // 模擬 request & response + // 對 PUT /users/1 發出 request,並夾帶 body.name = amdin2, body.email = admin_test@gmail.com + const req = mockRequest({ + params: { id: 1 }, + body: { name: 'admin2', email: 'admin_test@gmail.com' }, + }) + const res = mockResponse() + + // 測試作業指定的 userController.putUser 函式 + await this.userController.putUser(req, res) + + // putUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者資料編輯成功').should.be.true + // putUser 執行完畢後,應呼叫 res.redirect 並重新導向 /users/1 + res.redirect.calledWith('/users/1').should.be.true + // putUser 執行完畢後,id:1 使用者的 name 和 email 應該已被修改 + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 1 } }) + user.name.should.equal('admin2') + user.email.should.equal('admin_test@gmail.com') + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + }) +}) diff --git a/tests/R03.test.js b/tests/R03.test.js new file mode 100644 index 00000000..f415be50 --- /dev/null +++ b/tests/R03.test.js @@ -0,0 +1,59 @@ +const chai = require('chai') +const request = require('supertest') +const sinon = require('sinon') +const should = chai.should() + +const helpers = require('../_helpers') +const { createModelMock, createControllerProxy, mockRequest, mockResponse } = require('../helpers/unitTestHelpers'); + +describe('# R03: 餐廳資訊整理:Dashboard', function () { + context('# [Q1: Dashboard - 1 - controller / view / route]', () => { + before(async () => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', { + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }) + this.RestaurantMock = createModelMock('Restaurant', { + id: 1, + name: '銷魂麵', + viewCounts: 3 + }) + this.CategoryMock = createModelMock('Category', { + id: 1, + name: '食物' + }) + this.CommentMock = createModelMock('Comment', { + id: 1, + text: "gogogo" + }) + + // 連向模擬的 tables + this.restController = createControllerProxy('../controllers/restController', { + User: this.UserMock, + Category: this.CategoryMock, + Restaurant: this.RestaurantMock, + Comment: this.CommentMock, + }) + }) + + it(' GET /restaurants/:id/dashboard ', async () => { + // 模擬 request & response + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /restaurants/1/dashboard 發出請求 + const res = mockResponse() + // 測試 restController.getDashBoard 函式 + await this.restController.getDashBoard(req, res) + + // getDashBoard 執行完畢後,應呼叫 res.render + // res.render 的第 1 個參數要是 'dashboard' + // res.render 的第 2 個參數要包含 restaurant,其 name 屬性的值應是 '銷魂麵' + // res.render 的地 3 個參數要包含 restaurant,其 viewCounts 值應該是 3 + res.render.getCall(0).args[0].should.equal('dashboard') + res.render.getCall(0).args[1].restaurant.name.should.equal('銷魂麵') + res.render.getCall(0).args[1].restaurant.viewCounts.should.equal(3) + }) + }) +}) diff --git a/tests/R04.test.js b/tests/R04.test.js new file mode 100644 index 00000000..fecfb969 --- /dev/null +++ b/tests/R04.test.js @@ -0,0 +1,100 @@ +const chai = require('chai') +const request = require('supertest') +const sinon = require('sinon') +const should = chai.should() + +const db = require('../models') +const helpers = require('../_helpers') +const { createModelMock, createControllerProxy, mockRequest, mockResponse } = require('../helpers/unitTestHelpers') + +// 建立模擬的 Like 資料 +let mockLikeData = [ + { + UserId: 1, + RestaurantId: 2, + }, +] + +describe('# R04: Like / Unlike', function () { + context('# Q1: 使用者可以 Like 餐廳', () => { + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + + // 建立了一個模擬的 Like table,裡面目前是空的 + this.mockLikeData = [] + this.likeMock = createModelMock('Like', null, this.mockLikeData) + + // 連向模擬的 Like table + this.userController = createControllerProxy('../controllers/userController', {Like: this.likeMock}) + }) + + it(' POST /like/:restaurantId ', async () => { + // 模擬 request & response + const req = mockRequest({ params: { restaurantId: 2 } }) // 帶入 params.restaurantId = 2,對 POST /like/2 發出請求 + const res = mockResponse() + + // 測試 userController.addLike 函式 + await this.userController.addLike(req, res) + // 將模擬的 Like table 內的資料全數撈出 + const likes = await this.likeMock.findAll() + // addLike 執行完畢後,Like table 應會從空的 -> 變成有 1 筆資料 + likes.should.have.lengthOf(1) + // 資料裡的 UserId 應該會跟我們傳入的 user id 一樣 + likes[0].UserId.should.equal(1) + // 資料裡的 RestaurantId 會跟我們傳入的 params.restaurantId 一樣 + likes[0].RestaurantId.should.equal(2) + }) + + after(async () => { + // 清除驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context('# Q1: 使用者可以 unLike 餐廳', () => { + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + // 製作假資料 + // 下個 context 會用這筆資料進行測試 + // 模擬 Like table 裡目前有 1 筆資料如下 + this.likeMock = createModelMock('Like', { + id: 1, + UserId: 1, + RestaurantId: 2, + }, mockLikeData); + + // 連向模擬的 Like table + this.userController = createControllerProxy('../controllers/userController', { Like: this.likeMock }) + }) + + it(' DELETE /like/:restaurantId ', async () => { + // 模擬 request & response + // 模擬發出 request, 帶入 params.id = 1, restaurantId = 2 + const req = mockRequest({ params: { id: 1, restaurantId: 2 } }) // 帶入 params.id = 1,對 DELETE /like/2 發出請求 + const res = mockResponse() + + // 測試作業指定的 userController.removeLike 函式 + await this.userController.removeLike(req, res) + + // 將模擬的 Like table 內的資料全數撈出 + const likes = await this.likeMock.findAll() + // addLike 執行完畢後,Like table 應會從有 1 筆資料 -> 變成空的 + likes.should.have.lengthOf(0) + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) +}) diff --git a/tests/R05.test.js b/tests/R05.test.js new file mode 100644 index 00000000..3709204f --- /dev/null +++ b/tests/R05.test.js @@ -0,0 +1,146 @@ +const chai = require('chai') +const request = require('supertest') +const sinon = require('sinon') +const should = chai.should() + +const app = require('../app') +const helpers = require('../_helpers') +const { createModelMock, createControllerProxy, mockRequest, mockResponse } = require('../helpers/unitTestHelpers') + +// 模擬 2 間餐廳資料 +let mockRestaurantData = [ + { + id: 1, + name: 'Restaurant1', + tel: 'tel', + address: 'address', + opening_hours: 'opening_hours', + description: 'test description', + favoritedUsers: [ + { + userId: 1, + }, + ], + }, + { + id: 2, + name: 'Restaurant2', + tel: 'tel', + address: 'address', + opening_hours: 'opening_hours', + description: 'description', + categoryId: 1, + favoritedUsers: [], + }, +] + +describe('# R05: TOP 10 人氣餐廳 ', function () { + context('# [網址正確、畫面正常執行]', () => { + before(async () => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + // 模擬 getUser 函式,負責取得使用者資料 + this.getUser = sinon + .stub(helpers, 'getUser') + .returns({ id: 1, followings: [], favoritedRestaurants: [] }) + }) + + it(' GET /restaurants/top ', (done) => { + // 對 GET /restaurants/top 發出請求 + request(app) + .get('/restaurants/top') + .end(function (err, res) { + // 回應中應包含字串'Top 10 人氣餐廳' + // 若請求路徑正確,controller 執行後,會呼叫 res.render + // res.render 回傳的 view 樣板裡應包含字串'Top 10 人氣餐廳' + res.text.should.include('Top 10 人氣餐廳') + done() + }) + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context( + '# [當你點擊畫面上的「加入最愛 / 移除最愛」按鈕時,會重新計算「收藏數」的數字]', + () => { + before(async () => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + // 模擬 getUser 函式,負責取得使用者資料 + this.getUser = sinon + .stub(helpers, 'getUser') + .returns({ id: 1, followings: [], favoritedRestaurants: [] }) + + // 建立了一個模擬的 Restaurant table,裡面放入 2 間餐廳資料 + this.restaurantMock = createModelMock('Restaurant', null, mockRestaurantData) + + // 連向模擬的 Restaurant table + this.restController = createControllerProxy('../controllers/restController', { + Restaurant: this.restaurantMock, + }) + + // 建立了一個模擬的 Favorite table,裡面有 1 筆資料 + this.favoriteMock = createModelMock('Favorite', null, mockRestaurantData, 'FavoritedUsers') + + // 連向模擬的 Favorite table + this.userController = createControllerProxy('../controllers/userController', {Favorite: this.favoriteMock}) + }) + + it(' POST favorite ', async () => { + // 模擬 request & response + // 對 POST /favorite/2 發出請求,夾帶 user.favoritedRestaurants + const req = mockRequest({ + user: { favoritedRestaurants: [] }, + params: { restaurantId: 2 }, + }) + const res = mockResponse() + + // 測試 userController.addFavorite 函式 + await this.userController.addFavorite(req, res) + + // 取得餐廳排序資料 + await this.restController.getTopRestaurant(req, res) + + // addFavorite 執行完畢後,應呼叫 res.render + // res.render 的第 2 個參數要包含 restaurants + // restaurant 當中的第 2 筆資料 favoritedCount 屬性的值應是 1 (被 1 個使用者加入最愛了) + res.render.getCall(0).args[1].restaurants[1].favoritedCount.should.equal(1) + }) + + it(' DELETE favorite ', async () => { + // 模擬 request & response + // 對 DELETE /favorite/1 發出請求,夾入 user.favoritedRestaurants + const req = mockRequest({ + user: { favoritedRestaurants: [] }, + params: { restaurantId: 1 }, + }) + const res = mockResponse() + + // 測試 userController.removeFavorite 函式 + await this.userController.removeFavorite(req, res) + // 取得餐廳排序資料 + await this.restController.getTopRestaurant(req, res) + + // removeFavorite 執行完畢後,應呼叫 res.render + // res.render 的第 2 個參數要包含 restaurants + // restaurants 當中的第 1 筆資料 id 屬性值應是 2 (id:1 的那家餐廳被刪除了) + res.render.getCall(0).args[1].restaurants[0].id.should.equal(2) + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + } + ) +}) diff --git a/views/admin/categories.handlebars b/views/admin/categories.handlebars new file mode 100644 index 00000000..b2aca9c1 --- /dev/null +++ b/views/admin/categories.handlebars @@ -0,0 +1,53 @@ +
| # | +Category Name | +# | +
|---|---|---|
| {{this.id}} | +{{this.name}} | ++ + + | +
{{restaurant.Category.name}}
+{{restaurant.description}}
+| # | +Name | +Category | +# | +
|---|---|---|---|
| {{this.id}} | +{{this.name}} | +{{this.Category.name}} | ++ + + + | +
| # | +Role | +# | +|
|---|---|---|---|
| {{this.id}} | +{{this.email}} | +{{{showRole this.isAdmin}}} | ++ + | +
[{{restaurant.Category.name}}]
+{{this.text}}
+ by {{this.User.name}} + at {{moment this.createdAt}} +[{{restaurant.Category.name}}]
+{{restaurant.description}}
+ DashBoard + {{#if isFavorited }} + + {{else}} + + {{/if}} + {{#if isLiked }} + + {{else}} + + {{/if}} +++{{this.User.name}}
+{{this.text}}
+ +
{{this.description}}
+{{this.description}}
++ {{#if this.isFollowed }} +
+ {{else}} + + {{/if}} + +