diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..9427d58e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "emmet.optimizeStylesheetParsing": false +} \ 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/_helpers.js b/_helpers.js index 798e5325..61e51b67 100644 --- a/_helpers.js +++ b/_helpers.js @@ -1,12 +1,12 @@ function ensureAuthenticated(req) { - return req.isAuthenticated(); + return req.isAuthenticated() } function getUser(req) { - return req.user; + return req.user } module.exports = { ensureAuthenticated, getUser, -}; \ No newline at end of file +} diff --git a/app.js b/app.js index f70e64d6..e9d914a2 100644 --- a/app.js +++ b/app.js @@ -1,11 +1,41 @@ const express = require('express') +const flash = require('connect-flash') +const session = require('express-session') +const passport = require('./config/passport') const app = express() -const port = 3000 +const { engine } = require('express-handlebars') +const db = require('./models') // 引入資料庫 +const methodOverride = require('method-override') +const helpers = require('./_helpers') +const port = process.env.PORT || 3000 +if (process.env.NODE_ENV !== 'production') { + require('dotenv').config() +} + +app.use(express.urlencoded({ extended: true })) +app.use(session({ secret: 'secret', resave: false, saveUninitialized: false })) +app.use(flash()) +app.use(methodOverride('_method')) +app.use('/upload', express.static(__dirname + '/upload')) +// setup passport +app.use(passport.initialize()) +app.use(passport.session()) + +app.engine('handlebars', engine({ defaultLayout: 'main', helpers: require('./config/handlebars-helpers') })); +app.set('view engine', 'handlebars') + +// 把 req.flash 放到 res.locals 裡面 +app.use((req, res, next) => { + res.locals.success_messages = req.flash('success_messages') + res.locals.error_messages = req.flash('error_messages') + res.locals.user = req.user + next() +}) app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) }) -require('./routes')(app) +require('./routes')(app, passport) -module.exports = app +module.exports = app \ No newline at end of file diff --git a/config/config.json b/config/config.json index 93cd6cd8..3e5249ea 100644 --- a/config/config.json +++ b/config/config.json @@ -9,11 +9,21 @@ "production": { "username": "root", "password": null, - "database": "database_production", + "database": "database_test", "host": "127.0.0.1", "dialect": "mysql" }, - "travis": { + "production": { + "use_env_variable": "CLEARDB_DATABASE_URL", + "dialect": "postgres", + "protocol": "postgres", + "dialectOptions": { + "ssl": { + "rejectUnauthorized": false + } + } + }, + "travis": { "username": "travis", "database": "forum", "host": "127.0.0.1", diff --git a/config/handlebars-helpers.js b/config/handlebars-helpers.js new file mode 100644 index 00000000..0bd18dd8 --- /dev/null +++ b/config/handlebars-helpers.js @@ -0,0 +1,12 @@ +const moment = require('moment') +module.exports = { + 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/config/passport.js b/config/passport.js new file mode 100644 index 00000000..76550a32 --- /dev/null +++ b/config/passport.js @@ -0,0 +1,43 @@ +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 +const Like = db.Like + +// 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' } + ] + }).then(user => { + user = user.toJSON() + return cb(null, user) + }) +}) + +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..e6df963b --- /dev/null +++ b/controllers/adminController.js @@ -0,0 +1,169 @@ +const db = require('../models') +const Restaurant = db.Restaurant +const User = db.User +const fs = require('fs') +const imgur = require('imgur-node-api') +const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID +const Category = db.Category + +const adminController = { + //getRestaurants + getRestaurants: (req, res) => { + return Restaurant.findAll({ + raw: true, + nest: true, + include: [Category] + }).then(restaurants => { + return res.render('admin/restaurants', { restaurants }) + }) + }, + //getRestaurant + getRestaurant: (req, res) => { + return Restaurant.findByPk(req.params.id, { + include: [Category] + }).then(restaurant => { + return res.render('admin/restaurant', { + restaurant: restaurant.toJSON() + }) + }) + }, + /* create */ + // createPage + createRestaurant: (req, res) => { + Category.findAll({ + raw: true, + nest: true + }).then(categories => { + return res.render('admin/create', { categories }) + }) + }, + // POST to create + postRestaurant: (req, res) => { + if (!req.body.name) { + req.flash('error_messages', "name didn't exist") + return res.redirect('back') + } + 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) => { + req.flash('success_messages', 'restaurant was successfully created') + res.redirect('/admin/restaurants') + }) + }) + } + 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) => { + req.flash('success_messages', 'restaurant was successfully created') + return res.redirect('/admin/restaurants') + }) + } + }, + editRestaurant: async (req, res) => { + const categories = await Category.findAll({ + raw: true, + nest: true + }) + const restaurant = await Restaurant.findByPk(req.params.id) + return res.render('admin/create', { + categories: categories, + restaurant: restaurant.toJSON() + }) + }, + putRestaurant: (req, res) => { + if (!req.body.name) { + req.flash('error_messages', "name didn't exist") + return res.redirect('back') + } + 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) => { + req.flash('success_messages', 'restaurant was successfully to update') + res.redirect('/admin/restaurants') + }) + }) + }) + } 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, + CategoryId: req.body.categoryId + }).then((restaurant) => { + req.flash('success_messages', 'restaurant was successfully to update') + res.redirect('/admin/restaurants') + }) + }) + } + }, + deleteRestaurant: (req, res) => { + return Restaurant.findByPk(req.params.id) + .then((restaurant) => { + restaurant.destroy() + .then((restaurant) => { + res.redirect('/admin/restaurants') + }) + }) + }, + getUsers: (req, res) => { + return User.findAll({ raw: true }).then(users => { + return res.render('admin/users', { users }) + }) + }, + toggleAdmin: (req, res) => { + return User.findByPk(req.params.id) + .then( + (user) => { + if (user.email !== 'root@example.com') { + user.update({ + isAdmin: !user.isAdmin + }) + .then(() => { + req.flash('success_messages', '使用者權限變更成功') + res.redirect('/admin/users') + }) + } else { + req.flash('error_messages', '禁止變更管理者權限') + return res.redirect('back') + } + }) + } +} + +module.exports = adminController \ No newline at end of file diff --git a/controllers/categoryController.js b/controllers/categoryController.js new file mode 100644 index 00000000..f073fe4e --- /dev/null +++ b/controllers/categoryController.js @@ -0,0 +1,54 @@ +const db = require('../models') +const Category = db.Category +let categoryController = { + getCategories: (req, res) => { + return Category.findAll({ + raw: true, + nest: true + }).then(categories => { + return res.render('admin/categories', { categories }) + }) + }, + postCategory: (req, res) => { + if (!req.body.name) { + req.flash('error_messages', 'name didn\'t exist') + return res.redirect('back') + } else { + return Category.create({ + name: req.body.name + }) + .then((category) => { + res.redirect('/admin/categories') + }) + } + }, + getCategories: async (req, res) => { + const categories = await Category.findAll({ + raw: true, + nest: true + }) + if (req.params.id) { + const category = await Category.findByPk(req.params.id) + return res.render('admin/categories', { categories, category: category.toJSON() }) + } else { + return res.render('admin/categories', { categories }) + } + }, + putCategory: async (req, res) => { + if (!req.body.name) { + req.flash('error_messages', 'name didn\'t exist') + return res.redirect('back') + } else { + const category = await Category.findByPk(req.params.id) + await category.update(req.body) + res.redirect('/admin/categories') + } + }, + deleteCategory: async (req, res) => { + + const category = await Category.findByPk(req.params.id) + await category.destroy() + 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..49480e60 --- /dev/null +++ b/controllers/commentController.js @@ -0,0 +1,20 @@ +const db = require('../models') +const Comment = db.Comment + +const commentController = { + postComment: async (req, res) => { + await Comment.create({ + text: req.body.text, + RestaurantId: req.body.restaurantId, + UserId: req.user.id + }) + res.redirect(`/restaurants/${req.body.restaurantId}`) + }, + deleteComment: async (req, res) => { + const comment = await Comment.findByPk(req.params.id) + await comment.destroy() + 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..86a2aed1 --- /dev/null +++ b/controllers/restController.js @@ -0,0 +1,98 @@ +const { CommandCompleteMessage } = require('pg-protocol/dist/messages') +const db = require('../models') +const Restaurant = db.Restaurant +const Category = db.Category +const Comment = db.Comment +const User = db.User +const Like = db.Like + +const pageLimit = 10 + + +const restController = { + getRestaurants: async (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 + } + + const result = await Restaurant.findAndCountAll({ + include: Category, + where: whereQuery, + offset: offset, + limit: pageLimit + }) + // 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.Category.name, + isFavorited: req.user.FavoritedRestaurants.map(d => d.id).includes(r.id), + isLiked: req.user.LikedRestaurants.map(d => d.id).includes(r.id) + })) + //categories + const categories = await Category.findAll({ raw: true, nest: true }) + //render + return res.render('restaurants', { + restaurants: data, + categories, + categoryId, + page, + totalPage, + prev, + next + }) + }, + getRestaurant: async (req, res) => { + const restaurant = await Restaurant.findByPk(req.params.id, + { + include: [ + Category, + { model: User, as: 'FavoritedUsers' }, + { model: User, as: 'LikedUsers' }, + { model: Comment, include: [User] } + ] + }) + await restaurant.update({ ...restaurant.dataValues, viewcount: restaurant.viewcount + 1 }) + 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 res.render('restaurant', { restaurant: restaurant.toJSON(), isFavorited, isLiked }) + }, + getFeeds: async (req, res) => { + const restaurantsPromise = Restaurant.findAll({ + limit: 10, + raw: true, + nest: true, + order: [['createdAt', 'DESC']], + include: [Category] + }) + const commentsPromise = Comment.findAll({ + limit: 10, + raw: true, + nest: true, + order: [['createdAt', 'DESC']], + include: [User, Restaurant] + }) + const [restaurants, comments] = await Promise.all([restaurantsPromise, commentsPromise]) + return res.render('feeds', { restaurants, comments }) + }, + getDashBoard: async (req, res) => { + const restaurant = await Restaurant.findByPk(req.params.id, { include: [Category, Comment] }) + return res.render('dashboard', { restaurant: restaurant.toJSON() }) + }, +} + +module.exports = restController diff --git a/controllers/userController.js b/controllers/userController.js new file mode 100644 index 00000000..b069ca77 --- /dev/null +++ b/controllers/userController.js @@ -0,0 +1,148 @@ +const bcrypt = require('bcryptjs') +const db = require('../models') +const User = db.User +const fs = require('fs') +const imgur = require('imgur-node-api') +const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID +const helpers = require('../_helpers') +const Restaurant = db.Restaurant +const Comment = db.Comment +const Favorite = db.Favorite +const Like = db.Like + +const userController = { + signUpPage: (req, res) => { + return res.render('signup') + }, + signUp: (req, res) => { + User.create({ + name: req.body.name, + email: req.body.email, + password: bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10), null) + }).then(user => { + 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: async (req, res) => { + const user = await User.findByPk(req.params.id, { + include: [ + Comment, + { model: Comment, include: [Restaurant] } + ] + }) + return res.render('profile', { user: user.toJSON() }) + }, + // POST to create + putUser: (req, res) => { + if (req.params.id == helpers.getUser(req).id) { + if (!req.body.name) { + console.log(req.body) + console.log(req.user.name) + req.flash('error_messages', "name error") + return res.redirect('back') + } + const { file } = req + if (file) { + imgur.setClientID(IMGUR_CLIENT_ID); + imgur.upload(file.path, (err, img) => { + return User.findByPk(req.params.id) + .then(user => { + return user.update({ + name: req.body.name, + email: req.body.email ? req.body.email : getUser(req).email, + image: file ? img.data.link : user.image + }) + }).then((user) => { + req.flash('success_messages', '使用者資料編輯成功') + return res.redirect(`/users/${helpers.getUser(req).id}`) + }).catch(err => console.log(err)) + }) + } + else { + return User.findByPk(req.params.id) + .then(user => + user.update({ + name: req.body.name, + email: req.body.email ? req.body.email : getUser(req).email, + image: user.image + }).then((user) => { + req.flash('success_messages', '使用者資料編輯成功') + return res.redirect(`/users/${helpers.getUser(req).id}`) + }) + ) + } + } else { + req.flash('error_messages', '非使用者無編輯') + return res.redirect('/admin/restaurants') + } + }, + editUser: async (req, res) => { + if (req.params.id == helpers.getUser(req).id) { + const user = await User.findByPk(req.params.id) + res.render('edit', { user: user.toJSON() }) + } else { + req.flash('error_messages', '非本人無法編輯') + return res.redirect('back') + } + }, + addFavorite: (req, res) => { + return Favorite.create({ + UserId: helpers.getUser(req).id, + RestaurantId: req.params.restaurantId + }) + .then(() => { + return res.redirect('back') + }) + }, + removeFavorite: (req, res) => { + return Favorite.findOne({ + where: { + UserId: helpers.getUser(req).id, + RestaurantId: req.params.restaurantId + } + }) + .then((favorite) => { + favorite.destroy() + .then(() => { + return res.redirect('back') + }) + }) + }, + addLike: (req, res) => { + return Like.create({ + UserId: helpers.getUser(req).id, + RestaurantId: req.params.restaurantId + }) + .then(() => { + + return res.redirect('back') + }) + }, + removeLike: (req, res) => { + return Like.destroy({ + where: { + UserId: helpers.getUser(req).id, + RestaurantId: req.params.restaurantId + } + }) + .then((like) => { + return res.redirect('back') + }) + } +} + +module.exports = userController \ No newline at end of file diff --git a/helpers/unitTestHelpers.js b/helpers/unitTestHelpers.js new file mode 100644 index 00000000..4a777f4c --- /dev/null +++ b/helpers/unitTestHelpers.js @@ -0,0 +1,107 @@ +const SequelizeMock = require('sequelize-mock') +const proxyquire = require('proxyquire') +const sinon = require('sinon') + +const dbMock = new SequelizeMock() + +const createModelMock = (name, defaultValue, data, joinedTableName) => { + const mockModel = dbMock.define(name, defaultValue, { + instanceMethods: { + update: (changes) => { + mockModel._defaults = {...changes} + return Promise.resolve() + } + } + }); + + // 模擬 Sequelize 行為 + // 將 mock user db 中的 findByPK 用 findOne 取代 (sequelize mock not support findByPK) + mockModel.findByPk = (id) => mockModel.findOne({where: {id: id}}) + // 將 count 的 function 預設回傳假資料數目 1 + mockModel.count = () => 1 + // 因為 mock 中的 create 有問題,因此指向 upsert function, 這樣可以在 useHandler 中取得 create 呼叫 + mockModel.create = mockModel.upsert + + // modify middleware + if (joinedTableName) { + mockModel.$queryInterface.$useHandler((query, queryOptions) => { + if (query === 'upsert') { + // 新增 joinTable 資料到模擬資料 + const {UserId, RestaurantId} = queryOptions[0]; + const restaurant = data.find(d => d.id === RestaurantId) + restaurant[joinedTableName].push({UserId: UserId}); + return Promise.resolve(data.map(d => mockModel.build(d))) + } else if (query === 'findAll') { + // 回傳模擬資料 + if (!data) { + return mockModel.build([defaultValue]); + } + return Promise.resolve( data ? data.map(d => mockModel.build(d)) : []) + }else if (query === 'destroy') { + // destroy 可以從 where 取得要刪除的資料 + // 因此就可以模擬將模擬資料中的資料刪除 + // 刪除模擬資料中的某一筆 joinTable 資料 + const {UserId, RestaurantId} = queryOptions[0].where; + const restaurant = data.find(d => d.id === RestaurantId) + restaurant[joinedTableName] = restaurant[joinedTableName].filter(d => !(d.UserId === UserId)) + return Promise.resolve(data.map(d => mockModel.build(d))) + } + }); + } else { + mockModel.$queryInterface.$useHandler((query, queryOptions,done) => { + if (query === 'upsert') { + // create 時會帶 userId 跟 restaurantId (ex: Like.create({ userId: 1, restaurantId: 2})) + const {UserId, RestaurantId} = queryOptions[0] + + // 新增這個 Like 的資訊到模擬資料裡 + data.push({ UserId, RestaurantId }) + + // 回傳模擬資料 + return Promise.resolve(mockModel.build(data)) + } else if (query === 'findAll') { + // 回傳模擬資料 + if (!data) { + return mockModel.build([defaultValue]); + } + return Promise.resolve(data ? data.map(d => mockModel.build(d)) : []) + } else if (query === 'destroy') { + // destroy 可以從 where 取得要刪除的資料 + // 因此就可以模擬將模擬資料中的資料刪除 + const {UserId, RestaurantId} = queryOptions[0].where + data = data.filter(d => !(d.UserId === UserId && d.RestaurantId === RestaurantId)) + + return Promise.resolve(mockModel.build(data)) + } + }); + } + + return mockModel; +} + +const createControllerProxy = (path, model) => { + const controller = proxyquire(path, { + '../models': model + }); + + return controller; +} + +const mockRequest = (query) => { + return { + ...query, + flash: sinon.spy(), + } +} +const mockResponse = () => { + return { + redirect: sinon.spy(), + render: sinon.spy(), + } +} + +module.exports = { + createModelMock, + createControllerProxy, + mockRequest, + mockResponse +} \ No newline at end of file diff --git a/migrations/20211121064915-create-user.js b/migrations/20211121064915-create-user.js new file mode 100644 index 00000000..58a162b2 --- /dev/null +++ b/migrations/20211121064915-create-user.js @@ -0,0 +1,33 @@ +'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 + }, + email: { + type: Sequelize.STRING + }, + password: { + type: Sequelize.STRING + }, + 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/20211121091004-add-isAdmin-to-users.js b/migrations/20211121091004-add-isAdmin-to-users.js new file mode 100644 index 00000000..62a686e0 --- /dev/null +++ b/migrations/20211121091004-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'); + } +}; \ No newline at end of file diff --git a/migrations/20211121092333-create-restaurant.js b/migrations/20211121092333-create-restaurant.js new file mode 100644 index 00000000..9ae279d5 --- /dev/null +++ b/migrations/20211121092333-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/20211121095649-add-image-to-restaurants.js b/migrations/20211121095649-add-image-to-restaurants.js new file mode 100644 index 00000000..7aa07a53 --- /dev/null +++ b/migrations/20211121095649-add-image-to-restaurants.js @@ -0,0 +1,12 @@ +'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'); + } +}; \ No newline at end of file diff --git a/migrations/20211121182758-create-category.js b/migrations/20211121182758-create-category.js new file mode 100644 index 00000000..ab0ee89a --- /dev/null +++ b/migrations/20211121182758-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/20211121182826-add-categoryId-to-restaurant.js b/migrations/20211121182826-add-categoryId-to-restaurant.js new file mode 100644 index 00000000..c08e6d55 --- /dev/null +++ b/migrations/20211121182826-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') + } +} \ No newline at end of file diff --git a/migrations/20211122154533-create-comment.js b/migrations/20211122154533-create-comment.js new file mode 100644 index 00000000..8e42b57c --- /dev/null +++ b/migrations/20211122154533-create-comment.js @@ -0,0 +1,33 @@ +'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 + }, + RestaurantId: { + type: Sequelize.INTEGER + }, + 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/20211123130840-add-image-to-users.js b/migrations/20211123130840-add-image-to-users.js new file mode 100644 index 00000000..3f900d1a --- /dev/null +++ b/migrations/20211123130840-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 + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Users', 'image'); + } +}; \ No newline at end of file diff --git a/migrations/20211124130931-add-viewcount-to-restaurant.js b/migrations/20211124130931-add-viewcount-to-restaurant.js new file mode 100644 index 00000000..fe1caa0a --- /dev/null +++ b/migrations/20211124130931-add-viewcount-to-restaurant.js @@ -0,0 +1,14 @@ +'use strict' + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Restaurants', 'viewcount', { + type: Sequelize.INTEGER, + allowNull: false, + }) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Restaurants', 'viewcount') + } +} \ No newline at end of file diff --git a/migrations/20211124150250-create-favorite.js b/migrations/20211124150250-create-favorite.js new file mode 100644 index 00000000..6b9a5748 --- /dev/null +++ b/migrations/20211124150250-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/20211125150250-create-like.js b/migrations/20211125150250-create-like.js new file mode 100644 index 00000000..ad2df697 --- /dev/null +++ b/migrations/20211125150250-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/models/category.js b/models/category.js new file mode 100644 index 00000000..67536ff2 --- /dev/null +++ b/models/category.js @@ -0,0 +1,24 @@ +'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) { + // define association here + 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..ffec88de --- /dev/null +++ b/models/comment.js @@ -0,0 +1,27 @@ +'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) { + // define association here + Comment.belongsTo(models.Restaurant) + Comment.belongsTo(models.User) + } + }; + Comment.init({ + text: DataTypes.TEXT, + 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/index.js b/models/index.js index 66860356..33f09e77 100644 --- a/models/index.js +++ b/models/index.js @@ -1,41 +1,37 @@ -'use strict' +'use strict'; -const fs = require('fs') -const path = require('path') -const Sequelize = require('sequelize') -const basename = path.basename(__filename) -const env = process.env.NODE_ENV || 'development' -const config = require(__dirname + '/../config/config.json')[env] -const db = {} +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'development'; +const config = require(__dirname + '/../config/config.json')[env]; +const db = {}; -// 資料庫連線 -let sequelize +let sequelize; if (config.use_env_variable) { - sequelize = new Sequelize(process.env[config.use_env_variable], config) + sequelize = new Sequelize(process.env[config.use_env_variable], config); } else { - sequelize = new Sequelize(config.database, config.username, config.password, config) + sequelize = new Sequelize(config.database, config.username, config.password, config); } -// 動態引入其他 models fs .readdirSync(__dirname) .filter(file => { - return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js') + return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); }) .forEach(file => { - const model = sequelize['import'](path.join(__dirname, file)) - db[model.name] = model - }) + const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); + db[model.name] = model; + }); -// 設定 Models 之間的關聯 Object.keys(db).forEach(modelName => { if (db[modelName].associate) { - db[modelName].associate(db) + db[modelName].associate(db); } -}) +}); -// 匯出需要的物件 -db.sequelize = sequelize -db.Sequelize = Sequelize +db.sequelize = sequelize; +db.Sequelize = Sequelize; -module.exports = db +module.exports = db; 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..dc4997dd --- /dev/null +++ b/models/restaurant.js @@ -0,0 +1,42 @@ +'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) { + // define association here + 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, + CategoryId: DataTypes.INTEGER, + viewcount: 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..18718aa5 --- /dev/null +++ b/models/user.js @@ -0,0 +1,39 @@ +'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) + // define association here + User.belongsToMany(models.Restaurant, { + through: models.Favorite, + foreignKey: 'UserId', + as: 'FavoritedRestaurants' + }) + User.belongsToMany(models.Restaurant, { + through: models.Like, + foreignKey: 'UserId', + as: 'LikedRestaurants' + }) + + } + }; + 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 26dc7139..239eaaf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,9 +63,9 @@ } }, "@types/node": { - "version": "14.14.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.5.tgz", - "integrity": "sha512-H5Wn24s/ZOukBmDn03nnGTp18A60ny9AmCwnEcgJiTgSGsCO7k+NWP7zjCCbhlcnVCoI+co52dUAt9GMhOSULw==" + "version": "16.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", + "integrity": "sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==" }, "@ungap/promise-all-settled": { "version": "1.1.2", @@ -137,14 +137,15 @@ "dev": true }, "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -163,6 +164,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", @@ -183,17 +189,37 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, + "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=", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, + "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", @@ -221,6 +247,14 @@ "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", @@ -330,6 +364,43 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "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", @@ -447,22 +518,22 @@ "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" }, "cli-color": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz", - "integrity": "sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.1.tgz", + "integrity": "sha512-eBbxZF6fqPUNnf7CLAFOersUnyYzv83tHFLSlts+OAHsNendaqv2tHCq+/MO+b3Y+9JeoUlIvobyxG/Z8GNeOg==", "requires": { - "ansi-regex": "^2.1.1", - "d": "1", - "es5-ext": "^0.10.46", + "d": "^1.0.1", + "es5-ext": "^0.10.53", "es6-iterator": "^2.0.3", - "memoizee": "^0.4.14", - "timers-ext": "^0.1.5" + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" } }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, "requires": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", @@ -481,6 +552,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -488,7 +560,8 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true }, "combined-stream": { "version": "1.0.8", @@ -515,10 +588,50 @@ "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", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -537,6 +650,11 @@ "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", @@ -555,6 +673,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", @@ -566,6 +689,19 @@ "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", @@ -627,9 +763,9 @@ "dev": true }, "denque": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", - "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", + "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==" }, "depd": { "version": "1.1.2", @@ -641,6 +777,33 @@ "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", @@ -655,6 +818,11 @@ "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", @@ -705,7 +873,8 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true }, "encodeurl": { "version": "1.0.2", @@ -833,21 +1002,88 @@ "vary": "~1.1.2" } }, + "express-handlebars": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-6.0.1.tgz", + "integrity": "sha512-K3Lemki5jkD3sZwDhgBEBk+oAl1xg4nsMJAfpq1AUl5K187/mU1/xKVWt+4RZAHAxlyQFk4YBfX5+00AzLNfWg==", + "requires": { + "glob": "^7.2.0", + "graceful-fs": "^4.2.8", + "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", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", + "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", "requires": { - "type": "^2.0.0" + "type": "^2.5.0" }, "dependencies": { "type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", - "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", + "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==" } } }, + "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", @@ -890,6 +1126,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, "requires": { "locate-path": "^3.0.0" } @@ -900,6 +1137,11 @@ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true }, + "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", @@ -928,13 +1170,14 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" } }, "fs.realpath": { @@ -1045,6 +1288,18 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "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", @@ -1063,12 +1318,28 @@ "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==", "dev": true }, + "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", @@ -1099,6 +1370,14 @@ "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", @@ -1112,7 +1391,8 @@ "inflection": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=", + "dev": true }, "inflight": { "version": "1.0.6", @@ -1170,7 +1450,8 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true }, "is-glob": { "version": "4.0.1", @@ -1244,8 +1525,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "isexe": { "version": "2.0.0", @@ -1254,14 +1534,13 @@ "dev": true }, "js-beautify": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.13.0.tgz", - "integrity": "sha512-/Tbp1OVzZjbwzwJQFIlYLm9eWQ+3aYbBXLSaqb1mEJzhcQAfrqMMQYtjb6io+U6KpD0ID4F+Id3/xcjH3l/sqA==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.0.tgz", + "integrity": "sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ==", "requires": { "config-chain": "^1.1.12", "editorconfig": "^0.15.3", "glob": "^7.1.3", - "mkdirp": "^1.0.4", "nopt": "^5.0.0" } }, @@ -1280,12 +1559,18 @@ "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", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "requires": { - "graceful-fs": "^4.1.6" + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" } }, "just-extend": { @@ -1314,6 +1599,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -1386,18 +1672,25 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "memoizee": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", - "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==", + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", "requires": { - "d": "1", - "es5-ext": "^0.10.45", - "es6-weak-map": "^2.0.2", + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", "event-emitter": "^0.3.5", - "is-promise": "^2.1", - "lru-queue": "0.1", - "next-tick": "1", - "timers-ext": "^0.1.5" + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "dependencies": { + "next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + } } }, "merge-descriptors": { @@ -1405,6 +1698,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", @@ -1447,9 +1761,12 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + "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" + } }, "mocha": { "version": "8.2.0", @@ -1556,9 +1873,9 @@ "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" }, "moment-timezone": { - "version": "0.5.31", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz", - "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==", + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", + "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", "requires": { "moment": ">= 2.9.0" } @@ -1568,14 +1885,29 @@ "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" + } + }, "mysql2": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz", - "integrity": "sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", + "integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==", "requires": { - "denque": "^1.4.1", + "denque": "^2.0.1", "generate-function": "^2.3.1", - "iconv-lite": "^0.6.2", + "iconv-lite": "^0.6.3", "long": "^4.0.0", "lru-cache": "^6.0.0", "named-placeholders": "^1.1.2", @@ -1584,9 +1916,9 @@ }, "dependencies": { "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } @@ -1628,6 +1960,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", @@ -1725,6 +2062,16 @@ "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", @@ -1733,6 +2080,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", @@ -1758,6 +2110,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, "requires": { "p-limit": "^2.0.0" } @@ -1785,15 +2138,43 @@ } } }, + "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-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", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true }, "path-is-absolute": { "version": "1.0.1", @@ -1816,16 +2197,103 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, + "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", @@ -1883,6 +2351,11 @@ "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", @@ -1930,7 +2403,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -1961,6 +2433,64 @@ "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", @@ -2059,33 +2589,39 @@ "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" }, "sequelize": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.3.5.tgz", - "integrity": "sha512-MiwiPkYSA8NWttRKAXdU9h0TxP6HAc1fl7qZmMO/VQqQOND83G4nZLXd0kWILtAoT9cxtZgFqeb/MPYgEeXwsw==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.11.0.tgz", + "integrity": "sha512-+j3N5lr+FR1eicMRGR3bRsGOl9HMY0UGb2PyB2i1yZ64XBgsz3xejMH0UD45LcUitj40soDGIa9CyvZG0dfzKg==", "requires": { "debug": "^4.1.1", "dottie": "^2.0.0", - "inflection": "1.12.0", - "lodash": "^4.17.15", + "inflection": "1.13.1", + "lodash": "^4.17.20", "moment": "^2.26.0", "moment-timezone": "^0.5.31", + "pg-connection-string": "^2.5.0", "retry-as-promised": "^3.2.0", "semver": "^7.3.2", "sequelize-pool": "^6.0.0", "toposort-class": "^1.0.1", "uuid": "^8.1.0", - "validator": "^10.11.0", + "validator": "^13.7.0", "wkx": "^0.5.0" }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } }, + "inflection": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.1.tgz", + "integrity": "sha512-dldYtl2WlN0QDkIDtg8+xFwOS2Tbmp12t1cHa5/YClU6ZQjTFm7B66UcVbh9NQB+HvT5BAd2t5+yKsBkw5pcqA==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2094,17 +2630,145 @@ } }, "sequelize-cli": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.2.0.tgz", - "integrity": "sha512-6WQ2x91hg30dUn66mXHnzvHATZ4pyI1GHSNbS/TNN/vRR4BLRSLijadeMgC8zqmKDsL0VqzVVopJWfJakuP++Q==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.3.0.tgz", + "integrity": "sha512-+SkTDSeQdo93k7ZtSn5FCVXiMp+KMvkIrGtdLydLaR8TMoAHPpzw1AZCW6MAsL9M1VxRWoCKBFhzMG5gtcYNsQ==", "requires": { - "cli-color": "^1.4.0", - "fs-extra": "^7.0.1", + "cli-color": "^2.0.0", + "fs-extra": "^9.0.0", "js-beautify": "^1.8.8", "lodash": "^4.17.5", "resolve": "^1.5.0", "umzug": "^2.3.0", - "yargs": "^13.1.0" + "yargs": "^15.0.0" + }, + "dependencies": { + "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" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "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==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "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==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "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": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } } }, "sequelize-mock": { @@ -2178,6 +2842,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", @@ -2194,10 +2879,16 @@ "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", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -2208,7 +2899,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "requires": { "safe-buffer": "~5.2.0" }, @@ -2216,8 +2906,7 @@ "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==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } }, @@ -2225,6 +2914,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "requires": { "ansi-regex": "^4.1.0" }, @@ -2232,7 +2922,8 @@ "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true } } }, @@ -2358,6 +3049,11 @@ } } }, + "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", @@ -2383,6 +3079,11 @@ "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", @@ -2391,6 +3092,20 @@ "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", @@ -2413,9 +3128,9 @@ } }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" }, "unpipe": { "version": "1.0.0", @@ -2464,8 +3179,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", @@ -2473,14 +3187,14 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" }, "vary": { "version": "1.1.2", @@ -2588,6 +3302,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", @@ -2598,6 +3317,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, "requires": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", @@ -2625,6 +3345,11 @@ "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", @@ -2639,6 +3364,7 @@ "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, "requires": { "cliui": "^5.0.0", "find-up": "^3.0.0", @@ -2656,6 +3382,7 @@ "version": "13.1.2", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -2688,4 +3415,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index ec0db71d..e762cc20 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,25 @@ "url": "git+https://github.com/ALPHACamp/forum-express-grading.git" }, "dependencies": { + "bcryptjs": "^2.4.3", + "body-parser": "^1.19.0", + "connect-flash": "^0.1.1", + "dotenv": "^10.0.0", "express": "^4.17.1", - "mysql2": "^2.2.5", + "express-handlebars": "^6.0.1", + "express-session": "^1.17.2", + "faker": "^5.5.3", + "imgur-node-api": "^0.1.0", + "method-override": "^3.0.0", + "moment": "^2.29.1", + "multer": "^1.4.3", + "mysql2": "^2.3.3", "nodemon": "^2.0.12", - "sequelize": "^6.3.5", - "sequelize-cli": "^6.2.0" + "passport": "^0.5.0", + "passport-local": "^1.0.0", + "pg": "^8.7.1", + "sequelize": "^6.11.0", + "sequelize-cli": "^6.3.0" }, "devDependencies": { "chai": "^4.2.0", @@ -26,4 +40,4 @@ "sinon": "^9.2.0", "supertest": "^5.0.0" } -} +} \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index fc55e842..e0451e75 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,6 +1,76 @@ -module.exports = (app) => { +const restController = require('../controllers/restController.js') +const adminController = require('../controllers/adminController.js') +const userController = require('../controllers/userController.js') +const categoryController = require('../controllers/categoryController.js') +const commentController = require('../controllers/commentController.js') - app.get('/', (req, res) => { - res.send('Hello World!') - }) +const multer = require('multer') +const upload = multer({ dest: 'temp/' }) +const helpers = require('../_helpers') + +module.exports = (app, passport) => { + const authenticated = (req, res, next) => { + if (helpers.ensureAuthenticated(req)) { + return next() + } + res.redirect('/signin') + } + const authenticatedAdmin = (req, res, next) => { + if (helpers.ensureAuthenticated(req)) { + if (req.user.isAdmin) { return next() } + return res.redirect('/') + } + res.redirect('/signin') + } + + /* admin */ + // restaurants + app.get('/admin', authenticatedAdmin, (req, res) => res.redirect('/admin/restaurants')) + app.get('/admin/restaurants', authenticatedAdmin, adminController.getRestaurants) + app.get('/admin/restaurants/create', authenticatedAdmin, adminController.createRestaurant) //create page + app.post('/admin/restaurants', authenticatedAdmin, + upload.single('image'), adminController.postRestaurant) //create Restaurant (C) + app.get('/admin/restaurants/:id', authenticatedAdmin, adminController.getRestaurant) //read Restaurant (R) + app.get('/admin/restaurants/:id/edit', authenticatedAdmin, adminController.editRestaurant) //edit page + app.put('/admin/restaurants/:id', authenticatedAdmin + , upload.single('image'), adminController.putRestaurant) //edit Restaurant (U) + app.delete('/admin/restaurants/:id', authenticatedAdmin, adminController.deleteRestaurant) //delete Restaurant (D) + // users + app.get('/admin/users', authenticatedAdmin, adminController.getUsers) //read users (R) + app.put('/admin/users/:id/toggleAdmin', authenticatedAdmin, adminController.toggleAdmin) //edit users (U) + + // categories + app.get('/admin/categories', authenticatedAdmin, categoryController.getCategories) //categories page + app.post('/admin/categories', authenticatedAdmin, categoryController.postCategory) //create categorie (C) + app.get('/admin/categories/:id', authenticatedAdmin, categoryController.getCategories) //read categorie (R) + app.put('/admin/categories/:id', authenticatedAdmin, categoryController.putCategory) //edit categorie (U) + app.delete('/admin/categories/:id', authenticatedAdmin, categoryController.deleteCategory) //delete categorie (D) + + /* user */ + // restaurants + app.get('/', authenticated, (req, res) => res.redirect('/restaurants')) + app.get('/restaurants/feeds', authenticated, restController.getFeeds) //read feeds (R) + + app.get('/restaurants', authenticated, restController.getRestaurants) + app.get('/restaurants/:id', authenticated, restController.getRestaurant) //read Restaurant (R) + + app.get('/restaurants/:id/dashboard', authenticated, restController.getDashBoard) //read Dashboard (R) + app.get('/signup', userController.signUpPage) + app.post('/signup', userController.signUp) + app.get('/signin', userController.signInPage) + app.post('/signin', passport.authenticate('local', { failureRedirect: '/signin', failureFlash: true }), userController.signIn) + app.get('/logout', userController.logout) + //like + app.post('/like/:restaurantId', authenticated, userController.addLike) + app.delete('/like/:restaurantId', authenticated, userController.removeLike) + //favorite + app.post('/favorite/:restaurantId', authenticated, userController.addFavorite) + app.delete('/favorite/:restaurantId', authenticated, userController.removeFavorite) + // comments + app.post('/comments', authenticated, commentController.postComment) //create comment (C) + app.delete('/comments/:id', authenticatedAdmin, commentController.deleteComment)//delete comment (D) + //Profile + app.get('/users/:id', authenticated, userController.getUser) + app.get('/users/:id/edit', authenticated, userController.editUser) + app.put('/users/:id', authenticated, upload.single('image'), userController.putUser) } diff --git a/seeders/20211121112231-users-seed-file.js b/seeders/20211121112231-users-seed-file.js new file mode 100644 index 00000000..e8bcde55 --- /dev/null +++ b/seeders/20211121112231-users-seed-file.js @@ -0,0 +1,37 @@ +'use strict' +const bcrypt = require('bcryptjs') +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Users', [{ + id: 1, + email: 'root@example.com', + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + isAdmin: true, + name: 'root', + createdAt: new Date(), + updatedAt: new Date(), + image: `https://loremflickr.com/320/240/boy/?random=${Math.random() * 100}` + }, { + id: 11, + email: 'user1@example.com', + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + isAdmin: false, + name: 'user1', + createdAt: new Date(), + updatedAt: new Date(), + image: `https://loremflickr.com/320/240/boy/?random=${Math.random() * 100}` + }, { + id: 21, + email: 'user2@example.com', + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + isAdmin: false, + name: 'user2', + createdAt: new Date(), + updatedAt: new Date(), + image: `https://loremflickr.com/320/240/boy/?random=${Math.random() * 100}` + }], {}) + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Users', null, {}) + } +} diff --git a/seeders/20211121183430-categories-seed-file.js b/seeders/20211121183430-categories-seed-file.js new file mode 100644 index 00000000..7cb34e2e --- /dev/null +++ b/seeders/20211121183430-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) => { + await queryInterface.bulkDelete('Categories', null, {}) + } +} \ No newline at end of file diff --git a/seeders/20211122112506-restaurants-seed-file.js b/seeders/20211122112506-restaurants-seed-file.js new file mode 100644 index 00000000..b7b95363 --- /dev/null +++ b/seeders/20211122112506-restaurants-seed-file.js @@ -0,0 +1,27 @@ +'use strict' +const faker = require('faker') + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Restaurants', + Array.from({ length: 50 }).map((item, index) => + ({ + id: index * 10 + 1, + 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() * 7) * 10 + 1, + viewcount: 0 + }) + ), {}) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Restaurants', null, {}) + } +} \ No newline at end of file diff --git a/seeders/20211123162842-comment-seed-file.js b/seeders/20211123162842-comment-seed-file.js new file mode 100644 index 00000000..064aa7a7 --- /dev/null +++ b/seeders/20211123162842-comment-seed-file.js @@ -0,0 +1,21 @@ +'use strict' +const faker = require('faker') + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Comments', + Array.from({ length: 30 }).map(d => + ({ + UserId: Math.floor(Math.random() * 4) * 10 + 1, + RestaurantId: Math.floor(Math.random() * 50) * 10 + 1, + text: faker.lorem.text().substring(0, 50), + createdAt: new Date(), + updatedAt: new Date(), + }) + ), {}) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Comments', null, {}) + } +} \ No newline at end of file diff --git a/tests/R01.test.js b/tests/R01.test.js new file mode 100644 index 00000000..2765ccfb --- /dev/null +++ b/tests/R01.test.js @@ -0,0 +1,152 @@ +const chai = require('chai') +const request = require('supertest') +const should = chai.should() + +const app = require('../app') +const { createModelMock, createControllerProxy, mockRequest, mockResponse } = require('../helpers/unitTestHelpers'); + +describe('# R01', () => { + describe('登入測試: POST /signin', function(){ + // 以下測試會發出請求,測試資料庫內是否有作業指定的使用者資料 + // 測試資料的來源是真實的資料庫 + it('#1 密碼錯誤', function(done){ + request(app) + // 對 POST /signin 發出請求,參數是錯誤的密碼 + .post('/signin') + .type('urlencoded') + .send('email=root@example.com&password=123') + // 期待登入驗證回應失敗,重新導向 /signin + .expect('Location', '/signin') + .expect(302, done) + }) + + it('#2 帳號錯誤', function(done){ + request(app) + // 對 POST /signin 發出請求,參數是錯誤的帳號 + .post('/signin') + .type('urlencoded') + .send('email=tu&password=12345678') + // 期待登入驗證回應失敗,重新導向 /signin + .expect('Location', '/signin') + .expect(302, done) + }) + + it('#3 成功登入', function(done){ + request(app) + // 對 POST /signin 發出請求,參數是作業指定的使用者帳號密碼 + .post('/signin') + .type('urlencoded') + .send('email=root@example.com&password=12345678') + // 期待登入驗證成功,重新導向 /restaurants + .expect('Location', '/restaurants') + .expect(302, done) + }) + }); + + describe('# R01: 使用者權限管理', function () { + // 前置準備 + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', { + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }) + + // 修改 adminController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table + this.adminController = createControllerProxy('../controllers/adminController', { User: this.UserMock }) + }) + + // 開始測試 + context('# [顯示使用者清單]', () => { + it(' GET /admin/users ', async () => { + // 模擬 request & response + const req = mockRequest() // 對 GET /admin/users 發出請求 + const res = mockResponse() + + // 測試作業指定的 adminController.getUsers 函式 + await this.adminController.getUsers(req, res) + + // getUser 執行完畢後,應呼叫 res.render + // res.render 的第 2 個參數應是 users + // 根據測試資料,users 中的第 1 筆資料,name 屬性值應該要是 'admin' + res.render.getCall(0).args[1].users[0].name.should.equal('admin') + }) + }) + + context('# [修改使用者權限] for admin', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + { + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: true, // 是管理者 + } + ) + + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/adminController', { User: this.UserMock }) + }) + + it(' PUT /admin/users/:id/toggleAdmin ', async () => { + // 模擬 request & response + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PUT /admin/users/1/toggleAdmin 發出請求 + const res = mockResponse() + + // 測試作業指定的 adminController.toggleAdmin 函式 + await this.adminController.toggleAdmin(req, res) + + // toggleAdmin 正確執行的話,應呼叫 req.flash + // req.flash 的參數應該要與下列字串一致 + req.flash.calledWith('error_messages','禁止變更管理者權限').should.be.true + + // toggleAdmin 執行完畢後,應呼叫 res.redirect 並重新導向上一頁 + res.redirect.calledWith('back').should.be.true + }) + }) + + context('# [修改使用者權限] for user', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + { + id: 1, + email: 'user@example.com', + name: 'user', + isAdmin: false, // 非管理者 + } + ) + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/adminController', { User: this.UserMock }) + }) + + it(' PUT /admin/users/:id/toggleAdmin ', async () => { + // 模擬 request & response + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PUT /admin/users/1/toggleAdmin 發出請求 + const res = mockResponse() + + // 測試作業指定的 adminController.toggleAdmin 函式 + await this.adminController.toggleAdmin(req, res) + + // toggleAdmin 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + // toggleAdmin 執行完畢後,應呼叫 res.redirect 並重新導向 /admin/users + res.redirect.calledWith('/admin/users').should.be.true + + // toggleAmin 執行完畢後,假資料中 id:1 使用者的應該要是 isAdmin:true + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 1 } }) + user.isAdmin.should.equal(true) + }) + }) + }) +}) diff --git a/tests/R02.test.js b/tests/R02.test.js new file mode 100644 index 00000000..537e9144 --- /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..7915b826 --- /dev/null +++ b/tests/R04.test.js @@ -0,0 +1,101 @@ +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 筆資料 -> 變成空的 + console.log(likes) + likes.should.have.lengthOf(0) + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) +}) diff --git a/tests/R04.test.js~origin_R04-test b/tests/R04.test.js~origin_R04-test new file mode 100644 index 00000000..fecfb969 --- /dev/null +++ b/tests/R04.test.js~origin_R04-test @@ -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/index.js b/tests/index.js index b1d92960..57535e46 100644 --- a/tests/index.js +++ b/tests/index.js @@ -2,17 +2,14 @@ const request = require('supertest') const app = require('../app') -describe('# 測試環境初始化', function() { - +describe('# 測試環境初始化', function () { context('# First Test Case', () => { - - it(" GET /admin/users ", (done) => { - request(app) - .get('/') - .end(function(err, res) { - done() - }); - }); - + it(' GET /admin/users ', (done) => { + request(app) + .get('/') + .end(function (err, res) { + done() + }) + }) }) -}) \ No newline at end of file +}) diff --git a/views/admin/categories.handlebars b/views/admin/categories.handlebars new file mode 100644 index 00000000..f94e153b --- /dev/null +++ b/views/admin/categories.handlebars @@ -0,0 +1,62 @@ +
| # | +Category Name | +# | +
|---|---|---|
| {{this.id}} | +{{this.name}} | ++ + + | +
[{{restaurant.Category.name}}]
+{{restaurant.description}}
+| # | +Name | +Category | +# | +
|---|---|---|---|
| {{this.id}} | +{{this.name}} | +{{this.Category.name}} | ++ + + + | +
| # | +Name | +Name | +Role | +# | +|
|---|---|---|---|---|---|
| {{this.id}} | +{{this.name}} | +{{this.email}} | + {{#if this.isAdmin}} +admin | + {{else}} +user | + {{/if}} ++ + | +
[{{restaurant.Category.name}}]
+{{this.text}}
+ by + {{this.User.name}} + at + {{moment this.createdAt}} +{{user.Comments.length}} 已評論餐廳
+[{{restaurant.Category.name}}]
+{{restaurant.description}}
+ Dashboard + {{#if isFavorited }} + + {{else}} + + {{/if}} + {{#if isLiked }} + + {{else}} + + {{/if}} +++{{this.User.name}}
+{{this.text}}
+ +
{{this.description}}
+ {{#if this.isFavorited }} + + {{else}} + + {{/if}} + {{#if this.isLiked }} + + {{else}} + + {{/if}} +