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..862b9e4f 100644 --- a/app.js +++ b/app.js @@ -1,11 +1,46 @@ const express = require('express') +const exphbs = require('express-handlebars') +const db = require('./models') +const flash = require('connect-flash') +const session = require('express-session') +const passport = require('./config/passport') +const methodOverride = require('method-override') const app = express() -const port = 3000 +const port = process.env.PORT || 3000 + +if (process.env.NODE_ENV !== 'production') { + require('dotenv').config() +} + +app.engine('hbs', exphbs({extname: '.hbs'})) +app.set('view engine', 'hbs') + +// set body-parser +app.use(express.urlencoded({extended: true})) + +// 我們需要用到「快閃訊息 (flash message)」,這種 message 存在 session 裡面 +// set session and flash +app.use(session({ secret: 'secret', resave: false, saveUninitialized: false })) +app.use(flash()) +// set passport +app.use(passport.initialize()) +app.use(passport.session()) +// set method-override +app.use(methodOverride('_method')) +// 把 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() +}) +// /upload 這組路由,這裡因為是靜態檔案,所以不需要像其他的路由一樣寫 controller 邏輯,直接用 express.static 來指定路徑就可以了 +app.use('/upload', express.static(__dirname + '/upload')) app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) }) - -require('./routes')(app) +// 把 passport 傳入 routes +require('./routes')(app, passport) module.exports = app diff --git a/config/config.json b/config/config.json index 93cd6cd8..d891d0a9 100644 --- a/config/config.json +++ b/config/config.json @@ -4,14 +4,19 @@ "password": "password", "database": "forum", "host": "127.0.0.1", - "dialect": "mysql" + "dialect": "mysql", + "logging": true, + "logQueryParameter": false }, "production": { - "username": "root", - "password": null, - "database": "database_production", - "host": "127.0.0.1", - "dialect": "mysql" + "use_env_variable": "CLEARDB_DATABASE_URL", + "dialect": "postgres", + "protocol": "postgres", + "dialectOptions": { + "ssl": { + "rejectUnauthorized": false + } + } }, "travis": { "username": "travis", diff --git a/config/passport.js b/config/passport.js new file mode 100644 index 00000000..8906ee6d --- /dev/null +++ b/config/passport.js @@ -0,0 +1,38 @@ +const passport = require('passport') +const LocalStrategy = require('passport-local') +const bcrypt = require('bcryptjs') + +const db = require('../models') +const User = db.User + +// setup passport strategy +// passport.use(new LocalStrategy(option: 設定客製化選項, function 登入認證程序)) +passport.use(new LocalStrategy( + { + usernameField: 'email', + passwordField: 'password', + // 設置了 flash message,現在也想沿用,必須在 passport 的驗證邏輯裡,拿到 req 這個參數 + // 設定了 passReqToCallback: true,就可以 callback 的第一個參數裡拿到 req,這麼一來我們就可以呼叫 req.flash() 把想要客製化的訊息放進去 + passReqToCallback: true + }, + (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).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..732e2c4f --- /dev/null +++ b/controllers/adminController.js @@ -0,0 +1,142 @@ +const db = require('../models') +const fs = require('fs') +const imgur = require('imgur-node-api') +const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID +const Restaurant = db.Restaurant +const User = db.User + +const adminController = { + // get all Users + getUsers: (req, res) => { + return User.findAll({raw: true}).then((users) => { + return res.render('admin/users', { users: users }) + }) + }, + // toggle user isAdmin + toggleAdmin: (req, res) => { + return User.findByPk(req.params.id).then((user) => { + if (user.email === 'root@example.com') { + req.flash('error_messages', "禁止變更管理者權限") + return res.redirect('back') + } else { + user.update({ + isAdmin: !user.isAdmin + }).then((user) => { + req.flash('success_messages', '使用者權限變更成功') + res.redirect('/admin/users') + }) + } + }) + }, + // get all restaurants + getRestaurants: (req, res) => { + return Restaurant.findAll({ raw: true }).then(restaurants => { + return res.render('admin/restaurants', { restaurants: restaurants }) + }) + }, + // get one restaurant + getRestaurant: (req, res) => { + return Restaurant.findByPk(req.params.id, { raw: true }).then(restaurant => { + return res.render('admin/restaurant', { restaurant: restaurant }) + }) + }, + // create page + createRestaurant: (req, res) => { + return res.render('admin/create') + }, + // create new restaurant + postRestaurant: (req, res) => { + if (!req.body.name) { + req.flash('error_messages', "請填寫餐廳名稱") + 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 + }).then((restaurant) => { + req.flash('success_messages', '成功新增餐廳與圖片') + return 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 + }) + .then((restaurant) => { + req.flash('success_messages', '成功新增餐廳') + res.redirect('/admin/restaurants') + }) + } + }, + editRestaurant: (req, res) => { + return Restaurant.findByPk(req.params.id, { raw: true }).then((restaurant) => { + return res.render('admin/create', { restaurant: restaurant }) + }) + }, + 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 + }).then((restaurant) => { + req.flash('success_messages', '已成功更新餐廳資訊與圖片') + res.redirect(`/admin/restaurants/${req.params.id}`) + }) + }) + }) + } 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: null + }) + .then((restaurant) => { + req.flash('success_messages', '已成功更新餐廳資訊') + res.redirect(`/admin/restaurants/${req.params.id}`) + }) + }) + } + }, + deleteRestaurant: (req, res) => { + return Restaurant.findByPk(req.params.id) + .then((restaurant) => { + restaurant.destroy() + .then((restaurant) => { + req.flash('success_messages', '成功刪除餐廳') + res.redirect('/admin/restaurants') + }) + }) + } +} + +module.exports = adminController \ No newline at end of file diff --git a/controllers/restController.js b/controllers/restController.js new file mode 100644 index 00000000..631d9663 --- /dev/null +++ b/controllers/restController.js @@ -0,0 +1,7 @@ +const restController = { + getRestaurants: (req, res) => { + return res.render('restaurants') + } +} + +module.exports = restController \ No newline at end of file diff --git a/controllers/userController.js b/controllers/userController.js new file mode 100644 index 00000000..e8142ad2 --- /dev/null +++ b/controllers/userController.js @@ -0,0 +1,52 @@ +const bcrypt = require('bcryptjs') +const req = require('express/lib/request') +const { resetWatchers } = require('nodemon/lib/monitor/watch') +const db = require('../models') +const User = db.User + +const userController = { + SignUpPage: (req, res) => { + return res.render('signup') + }, + SignUp: (req, res) => { + // confirm + if (req.body.passwordCheck !== req.body.password) { + req.flash('error_messages', '兩次密碼輸入不同') + return res.redirect('/signup') + } else { + User.findOne({ where: { email: req.body.email } }) + .then(user => { + if (user) { + req.flash('error_messages', '信箱重複!') + return res.redirect('/signup') + } else { + User.create({ + name: req.body.name, + email: req.body.email, + password: bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10), null) + }).then(user => { + req.flash('success_messages', '成功註冊帳號!') + return res.redirect('/signin') + }) + } + }) + } + }, + signInPage: (req, res) => { + return res.render('signin') + }, + + signIn: (req, res) => { + req.flash('success_messages', '成功登入!') + res.redirect('/restaurants') + }, + + logout: (req, res) => { + req.flash('success_messages', '登出成功!') + req.logout() + res.redirect('/signin') + } + +} + +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/20211214140746-create-user.js b/migrations/20211214140746-create-user.js new file mode 100644 index 00000000..58a162b2 --- /dev/null +++ b/migrations/20211214140746-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/20211216133835-add-isAdmin-to-users.js b/migrations/20211216133835-add-isAdmin-to-users.js new file mode 100644 index 00000000..3875afa2 --- /dev/null +++ b/migrations/20211216133835-add-isAdmin-to-users.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Users', 'isAdmin', { + type: Sequelize.BOOLEAN, + // 寫入 MySQL 時會用 0 表示 false,不過實際在 app 裡呼叫 isAdmin 時會回傳 false,同時 True 寫入會變成 1。 + defaultValue: false, + }) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Users', 'isAdmin') + } +}; diff --git a/migrations/20211217051527-create-restaurant.js b/migrations/20211217051527-create-restaurant.js new file mode 100644 index 00000000..9fcc018a --- /dev/null +++ b/migrations/20211217051527-create-restaurant.js @@ -0,0 +1,40 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Restaurants', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + allowNull: false, + 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/20211221121301-add-image-to-restaurants.js b/migrations/20211221121301-add-image-to-restaurants.js new file mode 100644 index 00000000..4de2cc9a --- /dev/null +++ b/migrations/20211221121301-add-image-to-restaurants.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn ('Restaurants', 'image', {type: Sequelize.STRING}) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Restuarants', 'image') + } +}; 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/restaurant.js b/models/restaurant.js new file mode 100644 index 00000000..7ad9a01a --- /dev/null +++ b/models/restaurant.js @@ -0,0 +1,28 @@ +'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.init({ + name: DataTypes.STRING, + tel: DataTypes.STRING, + address: DataTypes.STRING, + opening_hours: DataTypes.STRING, + description: DataTypes.TEXT, + image: DataTypes.STRING + }, { + 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..abe49066 --- /dev/null +++ b/models/user.js @@ -0,0 +1,26 @@ +'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) { + // define association here + } + }; + User.init({ + name: DataTypes.STRING, + email: DataTypes.STRING, + password: DataTypes.STRING, + isAdmin: DataTypes.BOOLEAN + }, { + sequelize, + modelName: 'User', + }); + return User; +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 26dc7139..4ea7bb4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -163,6 +163,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 +188,32 @@ "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 }, + "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 +241,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 +358,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", @@ -515,6 +580,46 @@ "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", @@ -537,6 +642,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 +665,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 +681,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", @@ -641,6 +769,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 +810,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", @@ -833,6 +993,48 @@ "vary": "~1.1.2" } }, + "express-handlebars": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.2.0.tgz", + "integrity": "sha512-kkty9fsldSuqDI/5ohU+EWNOWaPhJOPmVUZewMH+7522atj8QF8WbXzjZKBRloafxRcsPDG68jmfW7MGCYT/1g==", + "requires": { + "glob": "^7.1.6", + "graceful-fs": "^4.2.4", + "handlebars": "^4.7.6" + } + }, + "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", @@ -848,6 +1050,11 @@ } } }, + "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", @@ -900,6 +1107,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", @@ -1045,6 +1257,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 +1287,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 +1339,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", @@ -1244,8 +1492,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", @@ -1280,6 +1527,11 @@ "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", @@ -1405,6 +1657,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", @@ -1568,6 +1841,31 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", + "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + } + } + }, "mysql2": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz", @@ -1628,6 +1926,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 +2028,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 +2046,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", @@ -1785,11 +2103,38 @@ } } }, + "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", @@ -1816,16 +2161,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.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "requires": { + "split2": "^4.1.0" + } + }, "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 +2315,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", @@ -1961,6 +2398,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", @@ -2178,6 +2673,24 @@ "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": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -2194,6 +2707,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -2358,6 +2876,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 +2906,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 +2919,20 @@ "is-typedarray": "^1.0.0" } }, + "uglify-js": { + "version": "3.14.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.5.tgz", + "integrity": "sha512-qZukoSxOG0urUTvjc2ERMTcAy+BiFh3weWAkeurLwjrCba73poHmG3E36XEjd/JGukMzwTL7uCxZiAexj8ppvQ==", + "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", @@ -2464,8 +3006,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", @@ -2588,6 +3129,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", @@ -2625,6 +3171,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", @@ -2688,4 +3239,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index ec0db71d..98841bfe 100644 --- a/package.json +++ b/package.json @@ -5,16 +5,29 @@ "scripts": { "start": "node app.js", "dev": "nodemon app.js", - "test": "mocha tests --exit --recursive --timeout 5000" + "test": "mocha tests --exit --recursive --timeout 5000", + "rebuild": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate:all" }, "repository": { "type": "git", "url": "git+https://github.com/ALPHACamp/forum-express-grading.git" }, "dependencies": { + "bcryptjs": "^2.4.3", + "connect-flash": "^0.1.1", + "dotenv": "^10.0.0", "express": "^4.17.1", + "express-handlebars": "^5.2.0", + "express-session": "^1.17.2", + "faker": "^5.5.3", + "imgur-node-api": "^0.1.0", + "method-override": "^3.0.0", + "multer": "^1.4.4", "mysql2": "^2.2.5", "nodemon": "^2.0.12", + "passport": "^0.5.0", + "passport-local": "^1.0.0", + "pg": "^8.7.1", "sequelize": "^6.3.5", "sequelize-cli": "^6.2.0" }, @@ -26,4 +39,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..9a971145 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,6 +1,62 @@ -module.exports = (app) => { +const restController = require('../controllers/restController.js') +const adminController = require('../controllers/adminController.js') +const userController = require('../controllers/userController.js') - app.get('/', (req, res) => { - res.send('Hello World!') - }) +// 這個temp資料夾是暫時的,通常定期會清空 +const multer = require('multer') +const upload = multer({dest: 'temp/'}) + +module.exports = (app, passport) => { + // 驗證登入 + const authenticated = (req, res, next) => { + if (req.isAuthenticated()) { + return next() + } + res.redirect('/signin') + } + const authenticatedAdmin = (req, res, next) => { + if (req.isAuthenticated()) { + if (req.user.isAdmin) { return next() } + return res.redirect('/') + } + res.redirect('/signin') + } + // 前台 + app.get('/', authenticated, (req, res) => res.redirect('/restaurants')) + app.get('/restaurants', authenticated, restController.getRestaurants) + // 後台 + app.get('/admin', authenticatedAdmin, (req, res) => res.redirect('/admin/restaurants')) + app.get('/admin/restaurants', authenticatedAdmin, adminController.getRestaurants) + // 註冊 + 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) + + // admin(後台) + app.get('/admin', (req, res) => res.redirect('/admin/restaurants')) + app.get('/admin/restaurants', adminController.getRestaurants) + app.get('/admin/users', authenticatedAdmin, adminController.getUsers) + // toggle user isAdmin + app.put('/admin/users/:id/toggleAdmin', authenticatedAdmin, adminController.toggleAdmin) + // go one Restaurant edit page + app.get('/admin/restaurants/:id/edit', authenticatedAdmin, adminController.editRestaurant) + // edit one Restaurnat + app.put('/admin/restaurants/:id', authenticatedAdmin, upload.single('image'), adminController.putRestaurant) + + // go create Restaurant page + app.get('/admin/restaurants/create', authenticatedAdmin, adminController.createRestaurant) + // create Restaurant + app.post('/admin/restaurants', authenticatedAdmin, upload.single('image'), adminController.postRestaurant) + + // get one Restaurant + app.get('/admin/restaurants/:id', authenticatedAdmin, adminController.getRestaurant) + + // delete one Restaurant + app.delete('/admin/restaurants/:id', authenticatedAdmin, adminController.deleteRestaurant) } + + diff --git a/seeders/20211221133217-users-seed-file.js b/seeders/20211221133217-users-seed-file.js new file mode 100644 index 00000000..44927f02 --- /dev/null +++ b/seeders/20211221133217-users-seed-file.js @@ -0,0 +1,32 @@ +'use strict'; +const bcrypt = require('bcryptjs') +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Users', [{ + email: 'root@example.com', + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + isAdmin: true, + name: 'root', + createdAt: new Date(), + updatedAt: new Date() + }, { + email: 'user1@example.com', + password: bcrypt.hashSync('1', bcrypt.genSaltSync(10), null), + isAdmin: false, + name: 'user1', + createdAt: new Date(), + updatedAt: new Date() + }, { + email: 'user2@example.com', + password: bcrypt.hashSync('2', bcrypt.genSaltSync(10), null), + isAdmin: false, + name: 'user2', + createdAt: new Date(), + updatedAt: new Date() + }], {}) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Users', null, {}) + } +}; diff --git a/seeders/20211221134241-restaurants-seed-file.js b/seeders/20211221134241-restaurants-seed-file.js new file mode 100644 index 00000000..592144e0 --- /dev/null +++ b/seeders/20211221134241-restaurants-seed-file.js @@ -0,0 +1,25 @@ +'use strict' +const faker = require('faker') + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Restaurants', + // 在留意 map 語法的時候,注意我們回傳餐廳資料物件時,有包了一個括號,這是在 arrow function 裡沒有寫 return 的情況下,可以用 () 來回傳 object literal + Array.from({ length: 50 }).map((d, i) => + ({ + name: faker.name.findName(), + tel: faker.phone.phoneNumber(), + address: faker.address.streetAddress(), + opening_hours: '08:00', + image: `https://loremflickr.com/320/240/restaurant,food/?random=${Math.random() * 100}`, + description: faker.lorem.text(), + createdAt: new Date(), + updatedAt: new Date() + }) + ), {}) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Restaurants', null, {}) + } +} 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/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/create.hbs b/views/admin/create.hbs new file mode 100644 index 00000000..6038a43c --- /dev/null +++ b/views/admin/create.hbs @@ -0,0 +1,42 @@ +{{#if restaurant}} +{{!-- 用表單寫入圖片,需要特地把格式改成 multipart/form-data,才能順利地把圖片一起 POST 進去 --}} +
\ No newline at end of file diff --git a/views/admin/restaurant.hbs b/views/admin/restaurant.hbs new file mode 100644 index 00000000..9fc6feac --- /dev/null +++ b/views/admin/restaurant.hbs @@ -0,0 +1,27 @@ +{{restaurant.description}}
+| # | +Name | +Action | +
|---|---|---|
| {{this.id}} | +{{this.name}} | ++ + + + | +
| # | +Name | +Role | +Authority | +|
|---|---|---|---|---|
| {{this.id}} | +{{this.name}} | +{{this.email}} | ++ {{#if this.isAdmin}} + admin + {{else}} + user + {{/if}} + | ++ + + | +