From fca7f3bc73dc9fa9bfadef198304b1ba88e6a44b Mon Sep 17 00:00:00 2001 From: Carrot7712 Date: Thu, 13 Jan 2022 16:48:07 +0800 Subject: [PATCH 01/38] feat: add R01.test.js and unit-test-helpers.js --- helpers/unit-test-helper.js | 143 ++++++++++++++++++++++++++ tests/R01.test.js | 195 ++++++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 helpers/unit-test-helper.js create mode 100644 tests/R01.test.js diff --git a/helpers/unit-test-helper.js b/helpers/unit-test-helper.js new file mode 100644 index 0000000000..ceed6030aa --- /dev/null +++ b/helpers/unit-test-helper.js @@ -0,0 +1,143 @@ +const SequelizeMock = require('sequelize-mock') +const proxyquire = require('proxyquire') +const sinon = require('sinon') + +const dbMock = new SequelizeMock() + +const createModelMock = (name, data, joinedTableName, sourceData) => { + const defaultValue = data[0] + const mockModel = dbMock.define(name, defaultValue, { + instanceMethods: { + update: function (changes) { + const objIndex = data.findIndex(d => d.id === this.get('id')) + data[objIndex] = { + ...data[objIndex], + ...changes + } + return Promise.resolve() + }, + destroy: function () { + if (joinedTableName) { + const userId = this.get('userId') + const restaurantId = this.get('restaurantId') + const restaurant = sourceData.find(d => d.id === restaurantId) + restaurant[joinedTableName] = restaurant[joinedTableName].filter(d => !(d.userId === userId)) + } else { + data = data && data.filter(d => d.userId !== this.get('userId') && d.restaurantId !== this.get('restaurantId')) // remove + } + } + } + }) + + // 模擬 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 = sourceData.find(d => d.id === restaurantId) + restaurant && restaurant[joinedTableName].push({ userId: userId }) + data.push(queryOptions[0]) + return Promise.resolve(data && 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 === 'findOne') { + const item = data.find(d => d.userId === queryOptions[0].where.userId && d.restaurantId === queryOptions[0].where.restaurantId) + if (!item) return null + + const result = mockModel.build(item) + return Promise.resolve(result) + } 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 === 'findOne') { + let item + if (queryOptions[0].id) { + item = data.find(d => d.id === queryOptions[0].id) + } else { + item = data.find(d => d.userId === queryOptions[0].where.userId && d.restaurantId === queryOptions[0].where.restaurantId) + } + if (!item) return null + + const result = mockModel.build(item) + return Promise.resolve(result) + } 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() + } +} + +const mockNext = err => console.log('[ERROR]:', err) + +module.exports = { + createModelMock, + createControllerProxy, + mockRequest, + mockResponse, + mockNext +} diff --git a/tests/R01.test.js b/tests/R01.test.js new file mode 100644 index 0000000000..574086d641 --- /dev/null +++ b/tests/R01.test.js @@ -0,0 +1,195 @@ +const request = require('supertest') +const chai = require('chai') +const sinon = require('sinon') +const should = chai.should() + +const app = require('../app') +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +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('# 使用者權限管理', function () { + // 前置準備 + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 修改 adminController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + // 開始測試 + context('# [顯示使用者清單]', () => { + it(' GET /admin/users ', async () => { + // 模擬 request & response & next + const req = mockRequest() // 對 GET /admin/users 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.getUsers 函式 + await this.adminController.getUsers(req, res, next) + + // 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 root', () => { + 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/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PATCH /admin/users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應該要與下列字串一致 + req.flash.calledWith('error_messages','禁止變更 root 權限').should.be.true + + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向上一頁 + res.redirect.calledWith('back').should.be.true + }) + }) + + context('# [修改使用者權限] for user (user -> admin)', () => { + 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/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PATCH /admin/users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users + res.redirect.calledWith('/admin/users').should.be.true + + // patchUser 執行完畢後,假資料中 id:1 使用者的應該要是 isAdmin:true + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 1 } }) + user.isAdmin.should.equal(true) + }) + }) + + context('# [修改使用者權限] for user (admin -> user)', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 2, + email: 'user2@example.com', + name: 'user2', + isAdmin: true // 是管理者 + }] + ) + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 2 } }) // 帶入 params.id = 2,對 PATCH /admin/users/2 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users + res.redirect.calledWith('/admin/users').should.be.true + + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 2 } }) + user.isAdmin.should.equal(false) + }) + }) + }) +}) \ No newline at end of file From 7ece79ef4c63264ebac70974f8c2bbce3c80e14c Mon Sep 17 00:00:00 2001 From: Amber Yan Date: Wed, 2 Mar 2022 15:23:34 +0800 Subject: [PATCH 02/38] add R01.test.js && unit-test-helper.js --- helpers/unit-test-helper.js | 143 ++++++++++++++++++++++++++ tests/R01.test.js | 195 ++++++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 helpers/unit-test-helper.js create mode 100644 tests/R01.test.js diff --git a/helpers/unit-test-helper.js b/helpers/unit-test-helper.js new file mode 100644 index 0000000000..ceed6030aa --- /dev/null +++ b/helpers/unit-test-helper.js @@ -0,0 +1,143 @@ +const SequelizeMock = require('sequelize-mock') +const proxyquire = require('proxyquire') +const sinon = require('sinon') + +const dbMock = new SequelizeMock() + +const createModelMock = (name, data, joinedTableName, sourceData) => { + const defaultValue = data[0] + const mockModel = dbMock.define(name, defaultValue, { + instanceMethods: { + update: function (changes) { + const objIndex = data.findIndex(d => d.id === this.get('id')) + data[objIndex] = { + ...data[objIndex], + ...changes + } + return Promise.resolve() + }, + destroy: function () { + if (joinedTableName) { + const userId = this.get('userId') + const restaurantId = this.get('restaurantId') + const restaurant = sourceData.find(d => d.id === restaurantId) + restaurant[joinedTableName] = restaurant[joinedTableName].filter(d => !(d.userId === userId)) + } else { + data = data && data.filter(d => d.userId !== this.get('userId') && d.restaurantId !== this.get('restaurantId')) // remove + } + } + } + }) + + // 模擬 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 = sourceData.find(d => d.id === restaurantId) + restaurant && restaurant[joinedTableName].push({ userId: userId }) + data.push(queryOptions[0]) + return Promise.resolve(data && 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 === 'findOne') { + const item = data.find(d => d.userId === queryOptions[0].where.userId && d.restaurantId === queryOptions[0].where.restaurantId) + if (!item) return null + + const result = mockModel.build(item) + return Promise.resolve(result) + } 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 === 'findOne') { + let item + if (queryOptions[0].id) { + item = data.find(d => d.id === queryOptions[0].id) + } else { + item = data.find(d => d.userId === queryOptions[0].where.userId && d.restaurantId === queryOptions[0].where.restaurantId) + } + if (!item) return null + + const result = mockModel.build(item) + return Promise.resolve(result) + } 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() + } +} + +const mockNext = err => console.log('[ERROR]:', err) + +module.exports = { + createModelMock, + createControllerProxy, + mockRequest, + mockResponse, + mockNext +} diff --git a/tests/R01.test.js b/tests/R01.test.js new file mode 100644 index 0000000000..574086d641 --- /dev/null +++ b/tests/R01.test.js @@ -0,0 +1,195 @@ +const request = require('supertest') +const chai = require('chai') +const sinon = require('sinon') +const should = chai.should() + +const app = require('../app') +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +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('# 使用者權限管理', function () { + // 前置準備 + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 修改 adminController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + // 開始測試 + context('# [顯示使用者清單]', () => { + it(' GET /admin/users ', async () => { + // 模擬 request & response & next + const req = mockRequest() // 對 GET /admin/users 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.getUsers 函式 + await this.adminController.getUsers(req, res, next) + + // 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 root', () => { + 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/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PATCH /admin/users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應該要與下列字串一致 + req.flash.calledWith('error_messages','禁止變更 root 權限').should.be.true + + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向上一頁 + res.redirect.calledWith('back').should.be.true + }) + }) + + context('# [修改使用者權限] for user (user -> admin)', () => { + 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/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PATCH /admin/users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users + res.redirect.calledWith('/admin/users').should.be.true + + // patchUser 執行完畢後,假資料中 id:1 使用者的應該要是 isAdmin:true + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 1 } }) + user.isAdmin.should.equal(true) + }) + }) + + context('# [修改使用者權限] for user (admin -> user)', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 2, + email: 'user2@example.com', + name: 'user2', + isAdmin: true // 是管理者 + }] + ) + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 2 } }) // 帶入 params.id = 2,對 PATCH /admin/users/2 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users + res.redirect.calledWith('/admin/users').should.be.true + + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 2 } }) + user.isAdmin.should.equal(false) + }) + }) + }) +}) \ No newline at end of file From b54a2e99ab2d9f4d1a6d4fe0393213518626296c Mon Sep 17 00:00:00 2001 From: Amber Yan Date: Wed, 2 Mar 2022 15:27:03 +0800 Subject: [PATCH 03/38] add R02.test.js file --- tests/R02.test.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/R02.test.js diff --git a/tests/R02.test.js b/tests/R02.test.js new file mode 100644 index 0000000000..728cce4dc5 --- /dev/null +++ b/tests/R02.test.js @@ -0,0 +1,60 @@ +const chai = require('chai') +const request = require('supertest') +const sinon = require('sinon') +const should = chai.should() + +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +describe('# R02: 餐廳資訊整理: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/restaurant-controller', { + User: this.UserMock, + Category: this.CategoryMock, + Restaurant: this.RestaurantMock, + Comment: this.CommentMock, + }) + }) + + it(' GET /restaurants/:id/dashboard ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /restaurants/1/dashboard 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試 restController.getDashBoard 函式 + await this.restController.getDashboard(req, res, next) + + // getDashBoard 正確執行的話,應呼叫 res.render + // res.render 的第 1 個參數要是 'dashboard' + // res.render 的第 2 個參數要包含 restaurant,其 name 屬性的值應是 '銷魂麵' + // res.render 的第 2 個參數要包含 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) + }) + }) +}) \ No newline at end of file From 79ff15885ef6a386efcfd22160781a580049ada1 Mon Sep 17 00:00:00 2001 From: zjzheng17 <90173539+zjzheng17@users.noreply.github.com> Date: Mon, 18 Jul 2022 12:36:56 +0800 Subject: [PATCH 04/38] =?UTF-8?q?=E8=A8=BB=E8=A7=A3=20typo=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/R01.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/R01.test.js b/tests/R01.test.js index 574086d641..92a989e23f 100644 --- a/tests/R01.test.js +++ b/tests/R01.test.js @@ -185,11 +185,11 @@ describe('# R01', () => { // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true - // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin: // 將假資料撈出,比對確認有成功修改到 const user = await this.UserMock.findOne({ where: { id: 2 } }) user.isAdmin.should.equal(false) }) }) }) -}) \ No newline at end of file +}) From 9d566b6e0de4de3ba4dc33137230dc8cce77a37b Mon Sep 17 00:00:00 2001 From: zjzheng17 <90173539+zjzheng17@users.noreply.github.com> Date: Mon, 18 Jul 2022 12:43:45 +0800 Subject: [PATCH 05/38] correcting typo in comment --- tests/R01.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/R01.test.js b/tests/R01.test.js index 574086d641..e635e363bf 100644 --- a/tests/R01.test.js +++ b/tests/R01.test.js @@ -185,11 +185,11 @@ describe('# R01', () => { // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true - // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:false // 將假資料撈出,比對確認有成功修改到 const user = await this.UserMock.findOne({ where: { id: 2 } }) user.isAdmin.should.equal(false) }) }) }) -}) \ No newline at end of file +}) From 03c14c01ab2502f04a830604c0e49974d3e3f654 Mon Sep 17 00:00:00 2001 From: zjzheng17 <90173539+zjzheng17@users.noreply.github.com> Date: Mon, 18 Jul 2022 12:56:31 +0800 Subject: [PATCH 06/38] correcting typo in comment --- tests/R01.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/R01.test.js b/tests/R01.test.js index 92a989e23f..e635e363bf 100644 --- a/tests/R01.test.js +++ b/tests/R01.test.js @@ -185,7 +185,7 @@ describe('# R01', () => { // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true - // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin: + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:false // 將假資料撈出,比對確認有成功修改到 const user = await this.UserMock.findOne({ where: { id: 2 } }) user.isAdmin.should.equal(false) From b53b8df27c0c371b6008cfcb761b398fc60bcece Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 12 Sep 2023 10:31:53 +0800 Subject: [PATCH 07/38] feat: add handlebars --- app.js | 4 +- package-lock.json | 125 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 1 + 3 files changed, 119 insertions(+), 11 deletions(-) diff --git a/app.js b/app.js index 0a97155def..abf7a20c68 100644 --- a/app.js +++ b/app.js @@ -1,9 +1,11 @@ const express = require('express') +const handlebars = require('express-handlebars') // 引入 express-handlebars const routes = require('./routes') const app = express() const port = process.env.PORT || 3000 - +app.engine('hbs', handlebars({ extname: '.hbs' })) +app.set('view engine', 'hbs') app.use(routes) app.listen(port, () => { diff --git a/package-lock.json b/package-lock.json index 7fdc1f3c0d..a755c67da3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.0.1", "dependencies": { "express": "^4.17.1", + "express-handlebars": "^5.3.3", "mysql2": "^2.3.0", "sequelize": "^6.6.5", "sequelize-cli": "^6.2.0" @@ -2051,6 +2052,19 @@ "node": ">= 0.10.0" } }, + "node_modules/express-handlebars": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.3.3.tgz", + "integrity": "sha512-/tWy/VmfdaZ5RUZUprgDzcCTj4QEnaXHbtLSXr4ROTpC/3aynrg4nIkn1+eLlcEp4Cs7HkxDlu2jJaz3aBG//w==", + "dependencies": { + "glob": "^7.1.7", + "graceful-fs": "^4.2.7", + "handlebars": "^4.7.7" + }, + "engines": { + "node": ">=v10.24.1" + } + }, "node_modules/ext": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", @@ -2441,9 +2455,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/growl": { "version": "1.10.5", @@ -2454,6 +2468,26 @@ "node": ">=4.x" } }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3323,8 +3357,7 @@ "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "node_modules/mkdirp": { "version": "1.0.4", @@ -3781,6 +3814,11 @@ "node": ">= 0.6" } }, + "node_modules/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==" + }, "node_modules/next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -4856,6 +4894,14 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5289,6 +5335,18 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", @@ -5551,6 +5609,11 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, "node_modules/workerpool": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", @@ -7265,6 +7328,16 @@ "vary": "~1.1.2" } }, + "express-handlebars": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.3.3.tgz", + "integrity": "sha512-/tWy/VmfdaZ5RUZUprgDzcCTj4QEnaXHbtLSXr4ROTpC/3aynrg4nIkn1+eLlcEp4Cs7HkxDlu2jJaz3aBG//w==", + "requires": { + "glob": "^7.1.7", + "graceful-fs": "^4.2.7", + "handlebars": "^4.7.7" + } + }, "ext": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", @@ -7567,9 +7640,9 @@ } }, "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "growl": { "version": "1.10.5", @@ -7577,6 +7650,18 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "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", @@ -8211,8 +8296,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { "version": "1.0.4", @@ -8553,6 +8637,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", @@ -9356,6 +9445,11 @@ } } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -9685,6 +9779,12 @@ "is-typedarray": "^1.0.0" } }, + "uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true + }, "umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", @@ -9886,6 +9986,11 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, "workerpool": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", diff --git a/package.json b/package.json index b6c7f1b9c7..5be3eca9b6 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "express": "^4.17.1", + "express-handlebars": "^5.3.3", "mysql2": "^2.3.0", "sequelize": "^6.6.5", "sequelize-cli": "^6.2.0" From c4156a43b6f497c926f81a6fc0fca606723c842e Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 12 Sep 2023 11:09:40 +0800 Subject: [PATCH 08/38] feat: add index page --- controllers/restaurant-controller.js | 6 ++++++ routes/index.js | 6 +++--- views/layouts/main.hbs | 23 +++++++++++++++++++++++ views/restaurants.hbs | 1 + 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 controllers/restaurant-controller.js create mode 100644 views/layouts/main.hbs create mode 100644 views/restaurants.hbs diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js new file mode 100644 index 0000000000..69753cab95 --- /dev/null +++ b/controllers/restaurant-controller.js @@ -0,0 +1,6 @@ +const restaurantController = { + getRestaurants: (req, res) => { + return res.render('restaurants') + } +} +module.exports = restaurantController diff --git a/routes/index.js b/routes/index.js index b4b2743f78..21ea6f8ad1 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,8 @@ const express = require('express') const router = express.Router() +const restController = require('../controllers/restaurant-controller') -router.get('/', (req, res) => { - res.send('Hello World!') -}) +router.get('/restaurants', restController.getRestaurants) +router.use('/', (req, res) => res.redirect('/restaurants')) module.exports = router diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs new file mode 100644 index 0000000000..a71645a84a --- /dev/null +++ b/views/layouts/main.hbs @@ -0,0 +1,23 @@ + + + + + + + 餐廳評論網 + + +
+
+
+ {{{body}}} +
+
+
+ + + + \ No newline at end of file diff --git a/views/restaurants.hbs b/views/restaurants.hbs new file mode 100644 index 0000000000..5fa22f43a6 --- /dev/null +++ b/views/restaurants.hbs @@ -0,0 +1 @@ +
restaurants
From 8487e9aed02ae34a897dfffcd3bc77c8f855cd85 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Thu, 14 Sep 2023 11:05:52 +0800 Subject: [PATCH 09/38] feat: add admin index page --- controllers/admin-controller.js | 6 ++++++ routes/index.js | 2 ++ routes/modules/admin.js | 6 ++++++ views/admin/restaurants.hbs | 1 + 4 files changed, 15 insertions(+) create mode 100644 controllers/admin-controller.js create mode 100644 routes/modules/admin.js create mode 100644 views/admin/restaurants.hbs diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js new file mode 100644 index 0000000000..b72c550ced --- /dev/null +++ b/controllers/admin-controller.js @@ -0,0 +1,6 @@ +const adminController = { + getRestaurants: (req, res) => { + return res.render("admin/restaurants"); + }, +}; +module.exports = adminController; diff --git a/routes/index.js b/routes/index.js index 21ea6f8ad1..d624f79232 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,6 +1,8 @@ const express = require('express') const router = express.Router() const restController = require('../controllers/restaurant-controller') +const admin = require('./modules/admin') //新增這行,載入 admin.js +router.use('/admin', admin) //新增這行 router.get('/restaurants', restController.getRestaurants) router.use('/', (req, res) => res.redirect('/restaurants')) diff --git a/routes/modules/admin.js b/routes/modules/admin.js new file mode 100644 index 0000000000..2f3854fffc --- /dev/null +++ b/routes/modules/admin.js @@ -0,0 +1,6 @@ +const express = require("express"); +const router = express.Router(); +const adminController = require("../../controllers/admin-controller"); +router.get("/restaurants", adminController.getRestaurants); +router.use("/", (req, res) => res.redirect("/admin/restaurants")); +module.exports = router; diff --git a/views/admin/restaurants.hbs b/views/admin/restaurants.hbs new file mode 100644 index 0000000000..fadb4711e0 --- /dev/null +++ b/views/admin/restaurants.hbs @@ -0,0 +1 @@ +
admin restaurants
\ No newline at end of file From 07e3f4ab5c9f399a8dc42371202d67c16dd1fa52 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Thu, 14 Sep 2023 11:16:49 +0800 Subject: [PATCH 10/38] feat: add user model --- controllers/admin-controller.js | 8 +++--- migrations/20230914031318-create-user.js | 33 ++++++++++++++++++++++++ models/user.js | 27 +++++++++++++++++++ routes/index.js | 4 +-- routes/modules/admin.js | 12 ++++----- 5 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 migrations/20230914031318-create-user.js create mode 100644 models/user.js diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index b72c550ced..56ba8a568a 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,6 +1,6 @@ const adminController = { getRestaurants: (req, res) => { - return res.render("admin/restaurants"); - }, -}; -module.exports = adminController; + return res.render('admin/restaurants') + } +} +module.exports = adminController diff --git a/migrations/20230914031318-create-user.js b/migrations/20230914031318-create-user.js new file mode 100644 index 0000000000..a99d1d2d71 --- /dev/null +++ b/migrations/20230914031318-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 + }, + created_at: { + allowNull: false, + type: Sequelize.DATE + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE + } + }) + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Users') + } +} diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000000..7385c96474 --- /dev/null +++ b/models/user.js @@ -0,0 +1,27 @@ +'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 + }, + { + sequelize, + modelName: 'User', + underscored: true + } + ) + return User +} diff --git a/routes/index.js b/routes/index.js index d624f79232..27285d8686 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,8 @@ const express = require('express') const router = express.Router() const restController = require('../controllers/restaurant-controller') -const admin = require('./modules/admin') //新增這行,載入 admin.js -router.use('/admin', admin) //新增這行 +const admin = require('./modules/admin') // 新增這行,載入 admin.js +router.use('/admin', admin) // 新增這行 router.get('/restaurants', restController.getRestaurants) router.use('/', (req, res) => res.redirect('/restaurants')) diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 2f3854fffc..7e8689462a 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -1,6 +1,6 @@ -const express = require("express"); -const router = express.Router(); -const adminController = require("../../controllers/admin-controller"); -router.get("/restaurants", adminController.getRestaurants); -router.use("/", (req, res) => res.redirect("/admin/restaurants")); -module.exports = router; +const express = require('express') +const router = express.Router() +const adminController = require('../../controllers/admin-controller') +router.get('/restaurants', adminController.getRestaurants) +router.use('/', (req, res) => res.redirect('/admin/restaurants')) +module.exports = router From 705f72e040f3a6f3bfe45b5d07cf1db065ace387 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Thu, 14 Sep 2023 11:33:53 +0800 Subject: [PATCH 11/38] feat: user signup --- app.js | 1 + controllers/user-controller.js | 23 +++++++++++++++++++++++ package-lock.json | 11 +++++++++++ package.json | 1 + routes/index.js | 8 ++++++-- views/signup.hbs | 26 ++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 controllers/user-controller.js create mode 100644 views/signup.hbs diff --git a/app.js b/app.js index abf7a20c68..50bf661d57 100644 --- a/app.js +++ b/app.js @@ -6,6 +6,7 @@ const app = express() const port = process.env.PORT || 3000 app.engine('hbs', handlebars({ extname: '.hbs' })) app.set('view engine', 'hbs') +app.use(express.urlencoded({ extended: true })); app.use(routes) app.listen(port, () => { diff --git a/controllers/user-controller.js b/controllers/user-controller.js new file mode 100644 index 0000000000..bc5cab2a47 --- /dev/null +++ b/controllers/user-controller.js @@ -0,0 +1,23 @@ +const bcrypt = require("bcryptjs"); //載入 bcrypt +const db = require("../models"); +const { User } = db; +const userController = { + signUpPage: (req, res) => { + res.render("signup"); + }, + signUp: (req, res) => { + bcrypt + .hash(req.body.password, 10) + .then((hash) => + User.create({ + name: req.body.name, + email: req.body.email, + password: hash, + }) + ) + .then(() => { + res.redirect("/signin"); + }); + }, +}; +module.exports = userController; diff --git a/package-lock.json b/package-lock.json index a755c67da3..e60400a544 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "forum-express-grading", "version": "3.0.1", "dependencies": { + "bcryptjs": "^2.4.3", "express": "^4.17.1", "express-handlebars": "^5.3.3", "mysql2": "^2.3.0", @@ -521,6 +522,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/binary-extensions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", @@ -6133,6 +6139,11 @@ "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": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "binary-extensions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", diff --git a/package.json b/package.json index 5be3eca9b6..bb636e8657 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "url": "git+https://github.com/ALPHACamp/forum-express-grading.git" }, "dependencies": { + "bcryptjs": "^2.4.3", "express": "^4.17.1", "express-handlebars": "^5.3.3", "mysql2": "^2.3.0", diff --git a/routes/index.js b/routes/index.js index 27285d8686..565a5db812 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,9 +1,13 @@ const express = require('express') const router = express.Router() +const admin = require("./modules/admin"); + const restController = require('../controllers/restaurant-controller') -const admin = require('./modules/admin') // 新增這行,載入 admin.js -router.use('/admin', admin) // 新增這行 +const userController = require("../controllers/user-controller"); +router.use('/admin', admin) // 新增這行 +router.get('/signup', userController.signUpPage) +router.post('/signup', userController.signUp) //注意用 post router.get('/restaurants', restController.getRestaurants) router.use('/', (req, res) => res.redirect('/restaurants')) diff --git a/views/signup.hbs b/views/signup.hbs new file mode 100644 index 0000000000..c7a13851a2 --- /dev/null +++ b/views/signup.hbs @@ -0,0 +1,26 @@ +
+

Sign Up

+
+ +
+

Sign In

+
\ No newline at end of file From bc142a7a91f5783bd717998cf2cc4f0189132522 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 19 Sep 2023 09:59:36 +0800 Subject: [PATCH 12/38] feat: flash msg & signup verification --- app.js | 15 +++- controllers/user-controller.js | 20 +++-- middleware/error-handler.js | 11 +++ package-lock.json | 145 +++++++++++++++++++++++++++++++++ package.json | 2 + routes/index.js | 3 + views/layouts/main.hbs | 1 + views/partials/messages.hbs | 18 ++++ 8 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 middleware/error-handler.js create mode 100644 views/partials/messages.hbs diff --git a/app.js b/app.js index 50bf661d57..daa5f549fd 100644 --- a/app.js +++ b/app.js @@ -1,12 +1,25 @@ const express = require('express') const handlebars = require('express-handlebars') // 引入 express-handlebars +const flash = require("connect-flash"); +const session = require("express-session"); const routes = require('./routes') const app = express() const port = process.env.PORT || 3000 +const SESSION_SECRET = "secret"; app.engine('hbs', handlebars({ extname: '.hbs' })) app.set('view engine', 'hbs') -app.use(express.urlencoded({ extended: true })); +app.use(express.urlencoded({ extended: true })) +app.use( + session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false }) +); +app.use(flash()); + +app.use((req, res, next) => { + res.locals.success_messages = req.flash("success_messages"); + res.locals.error_messages = req.flash("error_messages"); + next(); +}); app.use(routes) app.listen(port, () => { diff --git a/controllers/user-controller.js b/controllers/user-controller.js index bc5cab2a47..d989c2c036 100644 --- a/controllers/user-controller.js +++ b/controllers/user-controller.js @@ -5,9 +5,16 @@ const userController = { signUpPage: (req, res) => { res.render("signup"); }, - signUp: (req, res) => { - bcrypt - .hash(req.body.password, 10) + signUp: (req, res, next) => { + if (req.body.password !== req.body.passwordCheck) + throw new Error("Passwords do not match!"); + + User.findOne({ where: { email: req.body.email } }) + .then((user) => { + if (user) throw new Error("Email already exists!"); + + return bcrypt.hash(req.body.password, 10); + }) .then((hash) => User.create({ name: req.body.name, @@ -16,8 +23,11 @@ const userController = { }) ) .then(() => { + req.flash("success_messages", "成功註冊帳號!"); res.redirect("/signin"); - }); + }) + .catch((err) => next(err)); }, }; -module.exports = userController; + +module.exports = userController; \ No newline at end of file diff --git a/middleware/error-handler.js b/middleware/error-handler.js new file mode 100644 index 0000000000..991aa2476b --- /dev/null +++ b/middleware/error-handler.js @@ -0,0 +1,11 @@ +module.exports = { + generalErrorHandler(err, req, res, next) { + if (err instanceof Error) { + req.flash("error_messages", `${err.name}: ${err.message}`); + } else { + req.flash("error_messages", `${err}`); + } + res.redirect("back"); + next(err); + }, +}; diff --git a/package-lock.json b/package-lock.json index e60400a544..761a40d021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "3.0.1", "dependencies": { "bcryptjs": "^2.4.3", + "connect-flash": "^0.1.1", "express": "^4.17.1", "express-handlebars": "^5.3.3", + "express-session": "^1.17.2", "mysql2": "^2.3.0", "sequelize": "^6.6.5", "sequelize-cli": "^6.2.0" @@ -1022,6 +1024,14 @@ "node": ">=8" } }, + "node_modules/connect-flash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", + "integrity": "sha512-2rcfELQt/ZMP+SM/pG8PyhJRaLKp+6Hk2IUBNkEit09X+vwn3QsAL3ZbYtxUn7NVPzbMTSLRDhqe0B/eh30RYA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -2071,6 +2081,59 @@ "node": ">=v10.24.1" } }, + "node_modules/express-session": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz", + "integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express-session/node_modules/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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ext": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", @@ -4022,6 +4085,14 @@ "node": ">= 0.8" } }, + "node_modules/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==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4369,6 +4440,14 @@ "node": ">=0.6" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5353,6 +5432,17 @@ "node": ">=0.8.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", @@ -6529,6 +6619,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": "sha512-2rcfELQt/ZMP+SM/pG8PyhJRaLKp+6Hk2IUBNkEit09X+vwn3QsAL3ZbYtxUn7NVPzbMTSLRDhqe0B/eh30RYA==" + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -7349,6 +7444,38 @@ "handlebars": "^4.7.7" } }, + "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", @@ -8801,6 +8928,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", @@ -9065,6 +9197,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": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -9796,6 +9933,14 @@ "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/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", diff --git a/package.json b/package.json index bb636e8657..8d78ebb6ef 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ }, "dependencies": { "bcryptjs": "^2.4.3", + "connect-flash": "^0.1.1", "express": "^4.17.1", "express-handlebars": "^5.3.3", + "express-session": "^1.17.2", "mysql2": "^2.3.0", "sequelize": "^6.6.5", "sequelize-cli": "^6.2.0" diff --git a/routes/index.js b/routes/index.js index 565a5db812..b13ecaffab 100644 --- a/routes/index.js +++ b/routes/index.js @@ -4,11 +4,14 @@ const admin = require("./modules/admin"); const restController = require('../controllers/restaurant-controller') const userController = require("../controllers/user-controller"); +const { generalErrorHandler } = require("../middleware/error-handler"); + router.use('/admin', admin) // 新增這行 router.get('/signup', userController.signUpPage) router.post('/signup', userController.signUp) //注意用 post router.get('/restaurants', restController.getRestaurants) router.use('/', (req, res) => res.redirect('/restaurants')) +router.use("/", generalErrorHandler); module.exports = router diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs index a71645a84a..6b583c33db 100644 --- a/views/layouts/main.hbs +++ b/views/layouts/main.hbs @@ -11,6 +11,7 @@
+ {{> messages}} {{{body}}}
diff --git a/views/partials/messages.hbs b/views/partials/messages.hbs new file mode 100644 index 0000000000..fd311f3e2a --- /dev/null +++ b/views/partials/messages.hbs @@ -0,0 +1,18 @@ +{{#if error_messages}} + +{{/if}} +{{#if success_messages}} + +{{/if}} +{{#if warning_msg}} + +{{/if}} \ No newline at end of file From b9040ccde727ef1a534e57229354e70a6cf69838 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 19 Sep 2023 18:55:41 +0800 Subject: [PATCH 13/38] feat: passport init & signin --- app.js | 3 ++ config/passport.js | 53 +++++++++++++++++++++++++++ controllers/user-controller.js | 12 +++++++ package-lock.json | 65 ++++++++++++++++++++++++++++++++++ package.json | 2 ++ routes/index.js | 13 +++++++ views/signin.hbs | 19 ++++++++++ 7 files changed, 167 insertions(+) create mode 100644 config/passport.js create mode 100644 views/signin.hbs diff --git a/app.js b/app.js index daa5f549fd..2c7b6e9929 100644 --- a/app.js +++ b/app.js @@ -2,6 +2,7 @@ const express = require('express') const handlebars = require('express-handlebars') // 引入 express-handlebars const flash = require("connect-flash"); const session = require("express-session"); +const passport = require("./config/passport"); const routes = require('./routes') const app = express() @@ -13,6 +14,8 @@ app.use(express.urlencoded({ extended: true })) app.use( session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false }) ); +app.use(passport.initialize()); +app.use(passport.session()); app.use(flash()); app.use((req, res, next) => { diff --git a/config/passport.js b/config/passport.js new file mode 100644 index 0000000000..abd7401d80 --- /dev/null +++ b/config/passport.js @@ -0,0 +1,53 @@ +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( + // customize user field + { + usernameField: "email", + passwordField: "password", + passReqToCallback: true, + }, + // authenticate user + (req, email, password, cb) => { + User.findOne({ where: { email } }).then((user) => { + if (!user) + return cb( + null, + false, + req.flash("error_messages", "帳號或密碼輸入錯誤!") + ); + + bcrypt.compare(password, user.password).then((res) => { + if (!res) + 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() // 加入這行 + console.log(user) + return cb(null, user) + }) +}); + +module.exports = passport; diff --git a/controllers/user-controller.js b/controllers/user-controller.js index d989c2c036..2d0e856611 100644 --- a/controllers/user-controller.js +++ b/controllers/user-controller.js @@ -28,6 +28,18 @@ const userController = { }) .catch((err) => next(err)); }, + signInPage: (req, res) => { + 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/package-lock.json b/package-lock.json index 761a40d021..39fd4339b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "express-handlebars": "^5.3.3", "express-session": "^1.17.2", "mysql2": "^2.3.0", + "passport": "^0.4.1", + "passport-local": "^1.0.0", "sequelize": "^6.6.5", "sequelize-cli": "^6.2.0" }, @@ -4204,6 +4206,37 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -4248,6 +4281,11 @@ "node": "*" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -9016,6 +9054,28 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "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": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "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": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -9048,6 +9108,11 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", diff --git a/package.json b/package.json index 8d78ebb6ef..7cd09eb573 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "express-handlebars": "^5.3.3", "express-session": "^1.17.2", "mysql2": "^2.3.0", + "passport": "^0.4.1", + "passport-local": "^1.0.0", "sequelize": "^6.6.5", "sequelize-cli": "^6.2.0" }, diff --git a/routes/index.js b/routes/index.js index b13ecaffab..95b7c871f7 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,5 +1,7 @@ const express = require('express') const router = express.Router() +const passport = require("../config/passport"); + const admin = require("./modules/admin"); const restController = require('../controllers/restaurant-controller') @@ -10,6 +12,17 @@ const { generalErrorHandler } = require("../middleware/error-handler"); router.use('/admin', admin) // 新增這行 router.get('/signup', userController.signUpPage) router.post('/signup', userController.signUp) //注意用 post +router.get("/signin", userController.signInPage); +router.post( + "/signin", + passport.authenticate("local", { + failureRedirect: "/signin", + failureFlash: true, + }), + userController.signIn +); + +router.get("/logout", userController.logout); router.get('/restaurants', restController.getRestaurants) router.use('/', (req, res) => res.redirect('/restaurants')) router.use("/", generalErrorHandler); diff --git a/views/signin.hbs b/views/signin.hbs new file mode 100644 index 0000000000..68c557e1de --- /dev/null +++ b/views/signin.hbs @@ -0,0 +1,19 @@ +
+

Sign In

+
+ +
+

Sign Up

+
\ No newline at end of file From 2dfd00e2e817f546d34de80de742f9b9448afcfb Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 19 Sep 2023 19:25:56 +0800 Subject: [PATCH 14/38] feat: add header & footer --- app.js | 5 +++- config/passport.js | 50 ++++++++++++++++++---------------- controllers/user-controller.js | 45 +++++++++++++++--------------- helpers/auth-helpers.js | 6 ++++ helpers/handlebars-helpers.js | 5 ++++ package-lock.json | 11 ++++++++ package.json | 1 + views/layouts/main.hbs | 13 +++++---- views/partials/footer.hbs | 5 ++++ views/partials/header.hbs | 29 ++++++++++++++++++++ 10 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 helpers/auth-helpers.js create mode 100644 helpers/handlebars-helpers.js create mode 100644 views/partials/footer.hbs create mode 100644 views/partials/header.hbs diff --git a/app.js b/app.js index 2c7b6e9929..2e5cc50889 100644 --- a/app.js +++ b/app.js @@ -3,12 +3,14 @@ const handlebars = require('express-handlebars') // 引入 express-handlebars const flash = require("connect-flash"); const session = require("express-session"); const passport = require("./config/passport"); +const handlebarsHelpers = require("./helpers/handlebars-helpers"); +const { getUser } = require("./helpers/auth-helpers"); const routes = require('./routes') const app = express() const port = process.env.PORT || 3000 const SESSION_SECRET = "secret"; -app.engine('hbs', handlebars({ extname: '.hbs' })) +app.engine("hbs", handlebars({ extname: ".hbs", helpers: handlebarsHelpers })); app.set('view engine', 'hbs') app.use(express.urlencoded({ extended: true })) app.use( @@ -21,6 +23,7 @@ app.use(flash()); app.use((req, res, next) => { res.locals.success_messages = req.flash("success_messages"); res.locals.error_messages = req.flash("error_messages"); + res.locals.user = getUser(req); next(); }); app.use(routes) diff --git a/config/passport.js b/config/passport.js index abd7401d80..cba9506939 100644 --- a/config/passport.js +++ b/config/passport.js @@ -1,53 +1,55 @@ -const passport = require("passport"); -const LocalStrategy = require("passport-local"); -const bcrypt = require("bcryptjs"); -const db = require("../models"); -const User = db.User; +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( // customize user field { - usernameField: "email", - passwordField: "password", - passReqToCallback: true, + usernameField: 'email', + passwordField: 'password', + passReqToCallback: true }, // authenticate user (req, email, password, cb) => { - User.findOne({ where: { email } }).then((user) => { - if (!user) + User.findOne({ where: { email } }).then(user => { + if (!user) { return cb( null, false, - req.flash("error_messages", "帳號或密碼輸入錯誤!") - ); + req.flash('error_messages', '帳號或密碼輸入錯誤!') + ) + } - bcrypt.compare(password, user.password).then((res) => { - if (!res) + bcrypt.compare(password, user.password).then(res => { + if (!res) { return cb( null, false, - req.flash("error_messages", "帳號或密碼輸入錯誤!") - ); + req.flash('error_messages', '帳號或密碼輸入錯誤!') + ) + } - return cb(null, user); - }); - }); + return cb(null, user) + }) + }) } ) -); +) // serialize and deserialize user passport.serializeUser((user, cb) => { - cb(null, user.id); -}); + cb(null, user.id) +}) passport.deserializeUser((id, cb) => { User.findByPk(id).then(user => { user = user.toJSON() // 加入這行 console.log(user) return cb(null, user) }) -}); +}) -module.exports = passport; +module.exports = passport diff --git a/controllers/user-controller.js b/controllers/user-controller.js index 2d0e856611..79ebd0d1f0 100644 --- a/controllers/user-controller.js +++ b/controllers/user-controller.js @@ -1,45 +1,44 @@ -const bcrypt = require("bcryptjs"); //載入 bcrypt -const db = require("../models"); -const { User } = db; +const bcrypt = require('bcryptjs') // 載入 bcrypt +const db = require('../models') +const { User } = db const userController = { signUpPage: (req, res) => { - res.render("signup"); + res.render('signup') }, signUp: (req, res, next) => { - if (req.body.password !== req.body.passwordCheck) - throw new Error("Passwords do not match!"); + if (req.body.password !== req.body.passwordCheck) { throw new Error('Passwords do not match!') } User.findOne({ where: { email: req.body.email } }) - .then((user) => { - if (user) throw new Error("Email already exists!"); + .then(user => { + if (user) throw new Error('Email already exists!') - return bcrypt.hash(req.body.password, 10); + return bcrypt.hash(req.body.password, 10) }) - .then((hash) => + .then(hash => User.create({ name: req.body.name, email: req.body.email, - password: hash, + password: hash }) ) .then(() => { - req.flash("success_messages", "成功註冊帳號!"); - res.redirect("/signin"); + req.flash('success_messages', '成功註冊帳號!') + res.redirect('/signin') }) - .catch((err) => next(err)); + .catch(err => next(err)) }, signInPage: (req, res) => { - res.render("signin"); + res.render('signin') }, signIn: (req, res) => { - req.flash("success_messages", "成功登入!"); - res.redirect("/restaurants"); + req.flash('success_messages', '成功登入!') + res.redirect('/restaurants') }, logout: (req, res) => { - req.flash("success_messages", "登出成功!"); - req.logout(); - res.redirect("/signin"); - }, -}; + req.flash('success_messages', '登出成功!') + req.logout() + res.redirect('/signin') + } +} -module.exports = userController; \ No newline at end of file +module.exports = userController diff --git a/helpers/auth-helpers.js b/helpers/auth-helpers.js new file mode 100644 index 0000000000..a74485d425 --- /dev/null +++ b/helpers/auth-helpers.js @@ -0,0 +1,6 @@ +const getUser = (req) => { + return req.user || null; +}; +module.exports = { + getUser, +}; diff --git a/helpers/handlebars-helpers.js b/helpers/handlebars-helpers.js new file mode 100644 index 0000000000..73de1fee9f --- /dev/null +++ b/helpers/handlebars-helpers.js @@ -0,0 +1,5 @@ +const dayjs = require("dayjs"); + +module.exports = { + currentYear: () => dayjs().year(), +}; diff --git a/package-lock.json b/package-lock.json index 39fd4339b3..584a4c89ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "bcryptjs": "^2.4.3", "connect-flash": "^0.1.1", + "dayjs": "^1.10.6", "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", @@ -1104,6 +1105,11 @@ "type": "^1.0.1" } }, + "node_modules/dayjs": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", + "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6717,6 +6723,11 @@ "type": "^1.0.1" } }, + "dayjs": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", + "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index 7cd09eb573..7603c02d95 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "bcryptjs": "^2.4.3", "connect-flash": "^0.1.1", + "dayjs": "^1.10.6", "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs index 6b583c33db..18f85e2ae3 100644 --- a/views/layouts/main.hbs +++ b/views/layouts/main.hbs @@ -1,5 +1,5 @@ - + @@ -8,17 +8,20 @@ -
-
-
+ + {{> header}} +
+
+
{{> messages}} {{{body}}}
+ {{> footer}} + - \ No newline at end of file diff --git a/views/partials/footer.hbs b/views/partials/footer.hbs new file mode 100644 index 0000000000..e028efea43 --- /dev/null +++ b/views/partials/footer.hbs @@ -0,0 +1,5 @@ +
+
+ © 2017-{{ currentYear }} +
+
\ No newline at end of file diff --git a/views/partials/header.hbs b/views/partials/header.hbs new file mode 100644 index 0000000000..3873900eca --- /dev/null +++ b/views/partials/header.hbs @@ -0,0 +1,29 @@ +
+ +
\ No newline at end of file From 085cc7b381e37e405514d64670a8cb35f0419dbf Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 19 Sep 2023 21:27:00 +0800 Subject: [PATCH 15/38] feat: user model add is_admin --- helpers/auth-helpers.js | 9 +++++-- middleware/auth.js | 25 +++++++++++++++++++ .../20230919125657-add-is-admin-to-users.js | 14 +++++++++++ models/user.js | 9 ++++--- routes/index.js | 3 ++- routes/modules/admin.js | 4 ++- 6 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 middleware/auth.js create mode 100644 migrations/20230919125657-add-is-admin-to-users.js diff --git a/helpers/auth-helpers.js b/helpers/auth-helpers.js index a74485d425..491713ddb2 100644 --- a/helpers/auth-helpers.js +++ b/helpers/auth-helpers.js @@ -1,6 +1,11 @@ const getUser = (req) => { return req.user || null; -}; +} +const ensureAuthenticated = req => { + return req.isAuthenticated() +} + module.exports = { getUser, -}; + ensureAuthenticated +} \ No newline at end of file diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000000..f9220aa7e5 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,25 @@ +const { ensureAuthenticated, getUser } = require("../helpers/auth-helpers"); + +const authenticated = (req, res, next) => { + // if (req.isAuthenticated) + if (ensureAuthenticated(req)) { + return next(); + } + res.redirect("/signin"); +}; + +const authenticatedAdmin = (req, res, next) => { + // if (req.isAuthenticated) + if (ensureAuthenticated(req)) { + if (getUser(req).isAdmin) return next(); + + res.redirect("/"); + } else { + res.redirect("/signin"); + } +}; + +module.exports = { + authenticated, + authenticatedAdmin, +}; diff --git a/migrations/20230919125657-add-is-admin-to-users.js b/migrations/20230919125657-add-is-admin-to-users.js new file mode 100644 index 0000000000..1a14cabed0 --- /dev/null +++ b/migrations/20230919125657-add-is-admin-to-users.js @@ -0,0 +1,14 @@ +"use strict"; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn("Users", "is_admin", { + type: Sequelize.BOOLEAN, + defaultValue: false, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn("Users", "is_admin"); + }, +}; diff --git a/models/user.js b/models/user.js index 7385c96474..76f30efcc3 100644 --- a/models/user.js +++ b/models/user.js @@ -15,13 +15,14 @@ module.exports = (sequelize, DataTypes) => { { name: DataTypes.STRING, email: DataTypes.STRING, - password: DataTypes.STRING + password: DataTypes.STRING, + isAdmin: DataTypes.BOOLEAN }, { sequelize, - modelName: 'User', - underscored: true + modelName: "User", + underscored: true, } - ) + ); return User } diff --git a/routes/index.js b/routes/index.js index 95b7c871f7..e57aefd4b9 100644 --- a/routes/index.js +++ b/routes/index.js @@ -6,6 +6,7 @@ const admin = require("./modules/admin"); const restController = require('../controllers/restaurant-controller') const userController = require("../controllers/user-controller"); +const { authenticated } = require("../middleware/auth"); const { generalErrorHandler } = require("../middleware/error-handler"); @@ -23,7 +24,7 @@ router.post( ); router.get("/logout", userController.logout); -router.get('/restaurants', restController.getRestaurants) +router.get("/restaurants", authenticated, restController.getRestaurants); router.use('/', (req, res) => res.redirect('/restaurants')) router.use("/", generalErrorHandler); diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 7e8689462a..5a6165fec2 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -1,6 +1,8 @@ const express = require('express') const router = express.Router() const adminController = require('../../controllers/admin-controller') -router.get('/restaurants', adminController.getRestaurants) +const { authenticatedAdmin } = require("../../middleware/auth"); + +router.get("/restaurants", authenticatedAdmin, adminController.getRestaurants); router.use('/', (req, res) => res.redirect('/admin/restaurants')) module.exports = router From e980369b7c2ac0631201b5ae570d1ba7095b171d Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 19 Sep 2023 21:52:23 +0800 Subject: [PATCH 16/38] feat: add restaurant model --- .../20230919134454-create-restaurant.js | 39 +++++++++++++++++++ models/restaurant.js | 29 ++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 migrations/20230919134454-create-restaurant.js create mode 100644 models/restaurant.js diff --git a/migrations/20230919134454-create-restaurant.js b/migrations/20230919134454-create-restaurant.js new file mode 100644 index 0000000000..c4b16f51ee --- /dev/null +++ b/migrations/20230919134454-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, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("Restaurants"); + }, +}; diff --git a/models/restaurant.js b/models/restaurant.js new file mode 100644 index 0000000000..9f620a5bd8 --- /dev/null +++ b/models/restaurant.js @@ -0,0 +1,29 @@ +"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, + openingHours: DataTypes.STRING, + description: DataTypes.TEXT, + }, + { + sequelize, + modelName: "Restaurant", + underscored: true, + } + ); + return Restaurant; +}; From 5eb1fe4b96fe78611bee452664e160f6949e62df Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 19 Sep 2023 22:01:51 +0800 Subject: [PATCH 17/38] feat: modify admin restaurants page --- app.js | 32 +++++++-------- controllers/admin-controller.js | 17 +++++--- helpers/auth-helpers.js | 6 +-- helpers/handlebars-helpers.js | 6 +-- middleware/auth.js | 20 +++++----- middleware/error-handler.js | 14 +++---- .../20230919125657-add-is-admin-to-users.js | 14 +++---- .../20230919134454-create-restaurant.js | 30 +++++++------- models/restaurant.js | 18 ++++----- models/user.js | 6 +-- routes/index.js | 31 +++++++-------- routes/modules/admin.js | 4 +- views/admin/restaurants.hbs | 39 ++++++++++++++++++- 13 files changed, 140 insertions(+), 97 deletions(-) diff --git a/app.js b/app.js index 2e5cc50889..4868cf11dd 100644 --- a/app.js +++ b/app.js @@ -1,31 +1,31 @@ const express = require('express') const handlebars = require('express-handlebars') // 引入 express-handlebars -const flash = require("connect-flash"); -const session = require("express-session"); -const passport = require("./config/passport"); -const handlebarsHelpers = require("./helpers/handlebars-helpers"); -const { getUser } = require("./helpers/auth-helpers"); +const flash = require('connect-flash') +const session = require('express-session') +const passport = require('./config/passport') +const handlebarsHelpers = require('./helpers/handlebars-helpers') +const { getUser } = require('./helpers/auth-helpers') const routes = require('./routes') const app = express() const port = process.env.PORT || 3000 -const SESSION_SECRET = "secret"; -app.engine("hbs", handlebars({ extname: ".hbs", helpers: handlebarsHelpers })); +const SESSION_SECRET = 'secret' +app.engine('hbs', handlebars({ extname: '.hbs', helpers: handlebarsHelpers })) app.set('view engine', 'hbs') app.use(express.urlencoded({ extended: true })) app.use( session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false }) -); -app.use(passport.initialize()); -app.use(passport.session()); -app.use(flash()); +) +app.use(passport.initialize()) +app.use(passport.session()) +app.use(flash()) app.use((req, res, next) => { - res.locals.success_messages = req.flash("success_messages"); - res.locals.error_messages = req.flash("error_messages"); - res.locals.user = getUser(req); - next(); -}); + res.locals.success_messages = req.flash('success_messages') + res.locals.error_messages = req.flash('error_messages') + res.locals.user = getUser(req) + next() +}) app.use(routes) app.listen(port, () => { diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 56ba8a568a..52a7635516 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,6 +1,13 @@ +const { Restaurant } = require("../models"); + const adminController = { - getRestaurants: (req, res) => { - return res.render('admin/restaurants') - } -} -module.exports = adminController + getRestaurants: (req, res, next) => { + Restaurant.findAll({ + raw: true, + }) + .then((restaurants) => res.render("admin/restaurants", { restaurants })) + .catch((err) => next(err)); + }, +}; + +module.exports = adminController; diff --git a/helpers/auth-helpers.js b/helpers/auth-helpers.js index 491713ddb2..78a2f749a6 100644 --- a/helpers/auth-helpers.js +++ b/helpers/auth-helpers.js @@ -1,5 +1,5 @@ -const getUser = (req) => { - return req.user || null; +const getUser = req => { + return req.user || null } const ensureAuthenticated = req => { return req.isAuthenticated() @@ -8,4 +8,4 @@ const ensureAuthenticated = req => { module.exports = { getUser, ensureAuthenticated -} \ No newline at end of file +} diff --git a/helpers/handlebars-helpers.js b/helpers/handlebars-helpers.js index 73de1fee9f..04419ca97e 100644 --- a/helpers/handlebars-helpers.js +++ b/helpers/handlebars-helpers.js @@ -1,5 +1,5 @@ -const dayjs = require("dayjs"); +const dayjs = require('dayjs') module.exports = { - currentYear: () => dayjs().year(), -}; + currentYear: () => dayjs().year() +} diff --git a/middleware/auth.js b/middleware/auth.js index f9220aa7e5..015c9a5bbe 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,25 +1,25 @@ -const { ensureAuthenticated, getUser } = require("../helpers/auth-helpers"); +const { ensureAuthenticated, getUser } = require('../helpers/auth-helpers') const authenticated = (req, res, next) => { // if (req.isAuthenticated) if (ensureAuthenticated(req)) { - return next(); + return next() } - res.redirect("/signin"); -}; + res.redirect('/signin') +} const authenticatedAdmin = (req, res, next) => { // if (req.isAuthenticated) if (ensureAuthenticated(req)) { - if (getUser(req).isAdmin) return next(); + if (getUser(req).isAdmin) return next() - res.redirect("/"); + res.redirect('/') } else { - res.redirect("/signin"); + res.redirect('/signin') } -}; +} module.exports = { authenticated, - authenticatedAdmin, -}; + authenticatedAdmin +} diff --git a/middleware/error-handler.js b/middleware/error-handler.js index 991aa2476b..9fd5637155 100644 --- a/middleware/error-handler.js +++ b/middleware/error-handler.js @@ -1,11 +1,11 @@ module.exports = { - generalErrorHandler(err, req, res, next) { + generalErrorHandler (err, req, res, next) { if (err instanceof Error) { - req.flash("error_messages", `${err.name}: ${err.message}`); + req.flash('error_messages', `${err.name}: ${err.message}`) } else { - req.flash("error_messages", `${err}`); + req.flash('error_messages', `${err}`) } - res.redirect("back"); - next(err); - }, -}; + res.redirect('back') + next(err) + } +} diff --git a/migrations/20230919125657-add-is-admin-to-users.js b/migrations/20230919125657-add-is-admin-to-users.js index 1a14cabed0..648ee1d2af 100644 --- a/migrations/20230919125657-add-is-admin-to-users.js +++ b/migrations/20230919125657-add-is-admin-to-users.js @@ -1,14 +1,14 @@ -"use strict"; +'use strict' module.exports = { up: async (queryInterface, Sequelize) => { - await queryInterface.addColumn("Users", "is_admin", { + await queryInterface.addColumn('Users', 'is_admin', { type: Sequelize.BOOLEAN, - defaultValue: false, - }); + defaultValue: false + }) }, down: async (queryInterface, Sequelize) => { - await queryInterface.removeColumn("Users", "is_admin"); - }, -}; + await queryInterface.removeColumn('Users', 'is_admin') + } +} diff --git a/migrations/20230919134454-create-restaurant.js b/migrations/20230919134454-create-restaurant.js index c4b16f51ee..9f21213266 100644 --- a/migrations/20230919134454-create-restaurant.js +++ b/migrations/20230919134454-create-restaurant.js @@ -1,39 +1,39 @@ -"use strict"; +'use strict' module.exports = { up: async (queryInterface, Sequelize) => { - await queryInterface.createTable("Restaurants", { + await queryInterface.createTable('Restaurants', { id: { allowNull: false, autoIncrement: true, primaryKey: true, - type: Sequelize.INTEGER, + type: Sequelize.INTEGER }, name: { - type: Sequelize.STRING, + type: Sequelize.STRING }, tel: { - type: Sequelize.STRING, + type: Sequelize.STRING }, address: { - type: Sequelize.STRING, + type: Sequelize.STRING }, opening_hours: { - type: Sequelize.STRING, + type: Sequelize.STRING }, description: { - type: Sequelize.TEXT, + type: Sequelize.TEXT }, created_at: { allowNull: false, - type: Sequelize.DATE, + type: Sequelize.DATE }, updated_at: { allowNull: false, - type: Sequelize.DATE, - }, - }); + type: Sequelize.DATE + } + }) }, down: async (queryInterface, Sequelize) => { - await queryInterface.dropTable("Restaurants"); - }, -}; + await queryInterface.dropTable('Restaurants') + } +} diff --git a/models/restaurant.js b/models/restaurant.js index 9f620a5bd8..dec7b0988a 100644 --- a/models/restaurant.js +++ b/models/restaurant.js @@ -1,5 +1,5 @@ -"use strict"; -const { Model } = require("sequelize"); +'use strict' +const { Model } = require('sequelize') module.exports = (sequelize, DataTypes) => { class Restaurant extends Model { /** @@ -7,7 +7,7 @@ module.exports = (sequelize, DataTypes) => { * This method is not a part of Sequelize lifecycle. * The `models/index` file will call this method automatically. */ - static associate(models) { + static associate (models) { // define association here } } @@ -17,13 +17,13 @@ module.exports = (sequelize, DataTypes) => { tel: DataTypes.STRING, address: DataTypes.STRING, openingHours: DataTypes.STRING, - description: DataTypes.TEXT, + description: DataTypes.TEXT }, { sequelize, - modelName: "Restaurant", - underscored: true, + modelName: 'Restaurant', + underscored: true } - ); - return Restaurant; -}; + ) + return Restaurant +} diff --git a/models/user.js b/models/user.js index 76f30efcc3..318cbffbc4 100644 --- a/models/user.js +++ b/models/user.js @@ -20,9 +20,9 @@ module.exports = (sequelize, DataTypes) => { }, { sequelize, - modelName: "User", - underscored: true, + modelName: 'User', + underscored: true } - ); + ) return User } diff --git a/routes/index.js b/routes/index.js index e57aefd4b9..5bc58b4378 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,31 +1,30 @@ const express = require('express') const router = express.Router() -const passport = require("../config/passport"); +const passport = require('../config/passport') -const admin = require("./modules/admin"); +const admin = require('./modules/admin') const restController = require('../controllers/restaurant-controller') -const userController = require("../controllers/user-controller"); -const { authenticated } = require("../middleware/auth"); -const { generalErrorHandler } = require("../middleware/error-handler"); - +const userController = require('../controllers/user-controller') +const { authenticated } = require('../middleware/auth') +const { generalErrorHandler } = require('../middleware/error-handler') router.use('/admin', admin) // 新增這行 router.get('/signup', userController.signUpPage) -router.post('/signup', userController.signUp) //注意用 post -router.get("/signin", userController.signInPage); +router.post('/signup', userController.signUp) // 注意用 post +router.get('/signin', userController.signInPage) router.post( - "/signin", - passport.authenticate("local", { - failureRedirect: "/signin", - failureFlash: true, + '/signin', + passport.authenticate('local', { + failureRedirect: '/signin', + failureFlash: true }), userController.signIn -); +) -router.get("/logout", userController.logout); -router.get("/restaurants", authenticated, restController.getRestaurants); +router.get('/logout', userController.logout) +router.get('/restaurants', authenticated, restController.getRestaurants) router.use('/', (req, res) => res.redirect('/restaurants')) -router.use("/", generalErrorHandler); +router.use('/', generalErrorHandler) module.exports = router diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 5a6165fec2..7c3650db34 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -1,8 +1,8 @@ const express = require('express') const router = express.Router() const adminController = require('../../controllers/admin-controller') -const { authenticatedAdmin } = require("../../middleware/auth"); +const { authenticatedAdmin } = require('../../middleware/auth') -router.get("/restaurants", authenticatedAdmin, adminController.getRestaurants); +router.get('/restaurants', authenticatedAdmin, adminController.getRestaurants) router.use('/', (req, res) => res.redirect('/admin/restaurants')) module.exports = router diff --git a/views/admin/restaurants.hbs b/views/admin/restaurants.hbs index fadb4711e0..e509584e88 100644 --- a/views/admin/restaurants.hbs +++ b/views/admin/restaurants.hbs @@ -1 +1,38 @@ -
admin restaurants
\ No newline at end of file +

餐廳後台

+ + + +
+
+ + + + + + + + + + {{#each restaurants}} + + + + + + {{/each}} + +
#Name
{{this.id}}{{this.name}} + + +
+ +
+
\ No newline at end of file From e0ebb959cb1c550ed6fa340d04235d6bf332d8bd Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Wed, 20 Sep 2023 09:53:09 +0800 Subject: [PATCH 18/38] feat: create restaurant --- controllers/admin-controller.js | 22 +++++++++++++++++++++- routes/index.js | 4 ++-- routes/modules/admin.js | 6 ++++-- views/admin/create-restaurant.hbs | 26 ++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 views/admin/create-restaurant.hbs diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 52a7635516..1ecc87ac44 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -8,6 +8,26 @@ const adminController = { .then((restaurants) => res.render("admin/restaurants", { restaurants })) .catch((err) => next(err)); }, + createRestaurant: (req, res) => { + return res.render("admin/create-restaurant"); + }, + postRestaurant: (req, res, next) => { + const { name, tel, address, openingHours, description } = req.body; + if (!name) throw new Error("Restaurant name is required!"); + + Restaurant.create({ + name, + tel, + address, + openingHours, + description, + }) + .then(() => { + req.flash("success_messages", "restaurant was successfully created"); + res.redirect("/admin/restaurants"); + }) + .catch((err) => next(err)); + }, }; -module.exports = adminController; +module.exports = adminController; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 5bc58b4378..2c910bec8a 100644 --- a/routes/index.js +++ b/routes/index.js @@ -6,10 +6,10 @@ const admin = require('./modules/admin') const restController = require('../controllers/restaurant-controller') const userController = require('../controllers/user-controller') -const { authenticated } = require('../middleware/auth') +const { authenticated, authenticatedAdmin } = require("../middleware/auth"); const { generalErrorHandler } = require('../middleware/error-handler') -router.use('/admin', admin) // 新增這行 +router.use("/admin", authenticatedAdmin, admin); router.get('/signup', userController.signUpPage) router.post('/signup', userController.signUp) // 注意用 post router.get('/signin', userController.signInPage) diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 7c3650db34..f630a7485f 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -1,8 +1,10 @@ const express = require('express') const router = express.Router() const adminController = require('../../controllers/admin-controller') -const { authenticatedAdmin } = require('../../middleware/auth') -router.get('/restaurants', authenticatedAdmin, adminController.getRestaurants) + +router.get("/restaurants/create", adminController.createRestaurant); +router.get("/restaurants", adminController.getRestaurants); +router.post("/restaurants", adminController.postRestaurant); router.use('/', (req, res) => res.redirect('/admin/restaurants')) module.exports = router diff --git a/views/admin/create-restaurant.hbs b/views/admin/create-restaurant.hbs new file mode 100644 index 0000000000..c2f41fd391 --- /dev/null +++ b/views/admin/create-restaurant.hbs @@ -0,0 +1,26 @@ +
+
+

Create Restaurant

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
\ No newline at end of file From d0bb4f57aa842d9db4f8fb6067f0d711c2a9cafd Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Wed, 20 Sep 2023 10:10:28 +0800 Subject: [PATCH 19/38] feat: admin restaurant page --- controllers/admin-controller.js | 11 +++++++++++ routes/modules/admin.js | 1 + views/admin/restaurant.hbs | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 views/admin/restaurant.hbs diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 1ecc87ac44..f2a1a2a168 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -28,6 +28,17 @@ const adminController = { }) .catch((err) => next(err)); }, + getRestaurant: (req, res, next) => { + Restaurant.findByPk(req.params.id, { + raw: true, + }) + .then((restaurant) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); + + res.render("admin/restaurant", { restaurant }); + }) + .catch((err) => next(err)); + }, }; module.exports = adminController; \ No newline at end of file diff --git a/routes/modules/admin.js b/routes/modules/admin.js index f630a7485f..64a6e8617f 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -4,6 +4,7 @@ const adminController = require('../../controllers/admin-controller') router.get("/restaurants/create", adminController.createRestaurant); +router.get("/restaurants/:id", adminController.getRestaurant); router.get("/restaurants", adminController.getRestaurants); router.post("/restaurants", adminController.postRestaurant); router.use('/', (req, res) => res.redirect('/admin/restaurants')) diff --git a/views/admin/restaurant.hbs b/views/admin/restaurant.hbs new file mode 100644 index 0000000000..249fea3875 --- /dev/null +++ b/views/admin/restaurant.hbs @@ -0,0 +1,21 @@ +
+
+

{{restaurant.name}}

+
+ +
+
+
    +
  • Opening Hour: {{restaurant.openingHours}}
  • +
  • Tel: {{restaurant.tel}}
  • +
  • Address: {{restaurant.address}}
  • +
+
+
+
+

{{restaurant.description}}

+
+
+
+ +Back From 6b504c1af421655389206f0b9bc814bf8cd86e21 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Wed, 20 Sep 2023 10:46:46 +0800 Subject: [PATCH 20/38] feat: admin update restaurant --- app.js | 2 ++ controllers/admin-controller.js | 33 ++++++++++++++++++ package-lock.json | 44 ++++++++++++++++++++++++ package.json | 1 + routes/modules/admin.js | 2 ++ views/admin/create-restaurant.hbs | 26 ++------------ views/admin/edit-restaurant.hbs | 7 ++++ views/partials/admin-restaurant-form.hbs | 29 ++++++++++++++++ 8 files changed, 121 insertions(+), 23 deletions(-) create mode 100644 views/admin/edit-restaurant.hbs create mode 100644 views/partials/admin-restaurant-form.hbs diff --git a/app.js b/app.js index 4868cf11dd..7a1ada2caa 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,7 @@ const express = require('express') const handlebars = require('express-handlebars') // 引入 express-handlebars const flash = require('connect-flash') +const methodOverride = require("method-override"); const session = require('express-session') const passport = require('./config/passport') const handlebarsHelpers = require('./helpers/handlebars-helpers') @@ -19,6 +20,7 @@ app.use( app.use(passport.initialize()) app.use(passport.session()) app.use(flash()) +app.use(methodOverride("_method")); app.use((req, res, next) => { res.locals.success_messages = req.flash('success_messages') diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index f2a1a2a168..c12fc36bef 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -39,6 +39,39 @@ const adminController = { }) .catch((err) => next(err)); }, + editRestaurant: (req, res, next) => { + Restaurant.findByPk(req.params.id, { + raw: true, + }) + .then((restaurant) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); + + res.render("admin/edit-restaurant", { restaurant }); + }) + .catch((err) => next(err)); + }, + putRestaurant: (req, res, next) => { + const { name, tel, address, openingHours, description } = req.body; + if (!name) throw new Error("Restaurant name is required!"); + + Restaurant.findByPk(req.params.id) + .then((restaurant) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); + + return restaurant.update({ + name, + tel, + address, + openingHours, + description, + }); + }) + .then(() => { + req.flash("success_messages", "restaurant was successfully to update"); + res.redirect("/admin/restaurants"); + }) + .catch((err) => next(err)); + }, }; module.exports = adminController; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 584a4c89ba..aa6f008336 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", + "method-override": "^3.0.0", "mysql2": "^2.3.0", "passport": "^0.4.1", "passport-local": "^1.0.0", @@ -3373,6 +3374,28 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "node_modules/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==", + "dependencies": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/method-override/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -8443,6 +8466,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", diff --git a/package.json b/package.json index 7603c02d95..a89387f838 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", + "method-override": "^3.0.0", "mysql2": "^2.3.0", "passport": "^0.4.1", "passport-local": "^1.0.0", diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 64a6e8617f..920fc93c7c 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -4,7 +4,9 @@ const adminController = require('../../controllers/admin-controller') router.get("/restaurants/create", adminController.createRestaurant); +router.get("/restaurants/:id/edit", adminController.editRestaurant); router.get("/restaurants/:id", adminController.getRestaurant); +router.put("/restaurants/:id", adminController.putRestaurant); router.get("/restaurants", adminController.getRestaurants); router.post("/restaurants", adminController.postRestaurant); router.use('/', (req, res) => res.redirect('/admin/restaurants')) diff --git a/views/admin/create-restaurant.hbs b/views/admin/create-restaurant.hbs index c2f41fd391..a052c57a47 100644 --- a/views/admin/create-restaurant.hbs +++ b/views/admin/create-restaurant.hbs @@ -1,26 +1,6 @@ -
-
+ +

Create Restaurant

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- + {{> admin-restaurant-form}} \ No newline at end of file diff --git a/views/admin/edit-restaurant.hbs b/views/admin/edit-restaurant.hbs new file mode 100644 index 0000000000..0d457971b8 --- /dev/null +++ b/views/admin/edit-restaurant.hbs @@ -0,0 +1,7 @@ +
+
+

Edit Restaurant

+
+ + {{> admin-restaurant-form}} +
\ No newline at end of file diff --git a/views/partials/admin-restaurant-form.hbs b/views/partials/admin-restaurant-form.hbs new file mode 100644 index 0000000000..183a49db2e --- /dev/null +++ b/views/partials/admin-restaurant-form.hbs @@ -0,0 +1,29 @@ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + \ No newline at end of file From 151f4a0d578493f4fff09f99ea37b0b7835ba856 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Wed, 20 Sep 2023 21:05:34 +0800 Subject: [PATCH 21/38] feat: delete restaurant --- controllers/admin-controller.js | 74 +++++++++++++++++++-------------- routes/index.js | 4 +- routes/modules/admin.js | 14 +++---- views/admin/restaurants.hbs | 2 +- 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index c12fc36bef..3a44a3e6c1 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,77 +1,87 @@ -const { Restaurant } = require("../models"); +const { Restaurant } = require('../models') const adminController = { getRestaurants: (req, res, next) => { Restaurant.findAll({ - raw: true, + raw: true }) - .then((restaurants) => res.render("admin/restaurants", { restaurants })) - .catch((err) => next(err)); + .then(restaurants => res.render('admin/restaurants', { restaurants })) + .catch(err => next(err)) }, createRestaurant: (req, res) => { - return res.render("admin/create-restaurant"); + return res.render('admin/create-restaurant') }, postRestaurant: (req, res, next) => { - const { name, tel, address, openingHours, description } = req.body; - if (!name) throw new Error("Restaurant name is required!"); + const { name, tel, address, openingHours, description } = req.body + if (!name) throw new Error('Restaurant name is required!') Restaurant.create({ name, tel, address, openingHours, - description, + description }) .then(() => { - req.flash("success_messages", "restaurant was successfully created"); - res.redirect("/admin/restaurants"); + req.flash('success_messages', 'restaurant was successfully created') + res.redirect('/admin/restaurants') }) - .catch((err) => next(err)); + .catch(err => next(err)) }, getRestaurant: (req, res, next) => { Restaurant.findByPk(req.params.id, { - raw: true, + raw: true }) - .then((restaurant) => { - if (!restaurant) throw new Error("Restaurant didn't exist!"); + .then(restaurant => { + if (!restaurant) throw new Error("Restaurant didn't exist!") - res.render("admin/restaurant", { restaurant }); + res.render('admin/restaurant', { restaurant }) }) - .catch((err) => next(err)); + .catch(err => next(err)) }, editRestaurant: (req, res, next) => { Restaurant.findByPk(req.params.id, { - raw: true, + raw: true }) - .then((restaurant) => { - if (!restaurant) throw new Error("Restaurant didn't exist!"); + .then(restaurant => { + if (!restaurant) throw new Error("Restaurant didn't exist!") - res.render("admin/edit-restaurant", { restaurant }); + res.render('admin/edit-restaurant', { restaurant }) }) - .catch((err) => next(err)); + .catch(err => next(err)) }, putRestaurant: (req, res, next) => { - const { name, tel, address, openingHours, description } = req.body; - if (!name) throw new Error("Restaurant name is required!"); + const { name, tel, address, openingHours, description } = req.body + if (!name) throw new Error('Restaurant name is required!') Restaurant.findByPk(req.params.id) - .then((restaurant) => { - if (!restaurant) throw new Error("Restaurant didn't exist!"); + .then(restaurant => { + if (!restaurant) throw new Error("Restaurant didn't exist!") return restaurant.update({ name, tel, address, openingHours, - description, - }); + description + }) }) .then(() => { - req.flash("success_messages", "restaurant was successfully to update"); - res.redirect("/admin/restaurants"); + req.flash('success_messages', 'restaurant was successfully to update') + res.redirect('/admin/restaurants') }) - .catch((err) => next(err)); + .catch(err => next(err)) }, -}; + deleteRestaurant: (req, res, next) => { + return Restaurant.findByPk(req.params.id) + .then(restaurant => { + if (!restaurant) throw new Error("Restaurant didn't exist!") -module.exports = adminController; \ No newline at end of file + return restaurant.destroy() + }) + .then(() => res.redirect('/admin/restaurants')) + .catch(err => next(err)) + } +} + +module.exports = adminController; diff --git a/routes/index.js b/routes/index.js index 2c910bec8a..cd1e58ed8e 100644 --- a/routes/index.js +++ b/routes/index.js @@ -6,10 +6,10 @@ const admin = require('./modules/admin') const restController = require('../controllers/restaurant-controller') const userController = require('../controllers/user-controller') -const { authenticated, authenticatedAdmin } = require("../middleware/auth"); +const { authenticated, authenticatedAdmin } = require('../middleware/auth') const { generalErrorHandler } = require('../middleware/error-handler') -router.use("/admin", authenticatedAdmin, admin); +router.use('/admin', authenticatedAdmin, admin) router.get('/signup', userController.signUpPage) router.post('/signup', userController.signUp) // 注意用 post router.get('/signin', userController.signInPage) diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 920fc93c7c..16db28ec74 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -2,12 +2,12 @@ const express = require('express') const router = express.Router() const adminController = require('../../controllers/admin-controller') - -router.get("/restaurants/create", adminController.createRestaurant); -router.get("/restaurants/:id/edit", adminController.editRestaurant); -router.get("/restaurants/:id", adminController.getRestaurant); -router.put("/restaurants/:id", adminController.putRestaurant); -router.get("/restaurants", adminController.getRestaurants); -router.post("/restaurants", adminController.postRestaurant); +router.get('/restaurants/create', adminController.createRestaurant) +router.get('/restaurants/:id/edit', adminController.editRestaurant) +router.get('/restaurants/:id', adminController.getRestaurant) +router.put('/restaurants/:id', adminController.putRestaurant) +router.delete('/restaurants/:id', adminController.deleteRestaurant) +router.get('/restaurants', adminController.getRestaurants) +router.post('/restaurants', adminController.postRestaurant) router.use('/', (req, res) => res.redirect('/admin/restaurants')) module.exports = router diff --git a/views/admin/restaurants.hbs b/views/admin/restaurants.hbs index e509584e88..a3d44b5cdb 100644 --- a/views/admin/restaurants.hbs +++ b/views/admin/restaurants.hbs @@ -29,7 +29,7 @@
- +
From 92d1d24998277c1bc7b31d8d74837f931799b39f Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Wed, 20 Sep 2023 22:17:53 +0800 Subject: [PATCH 22/38] feat: add restaurant image --- app.js | 6 +- controllers/admin-controller.js | 46 ++- helpers/file-helpers.js | 18 + middleware/multer.js | 4 + ...20230920130606-add-image-to-restaurants.js | 11 + models/restaurant.js | 9 +- package-lock.json | 331 +++++++++++++++++- package.json | 1 + routes/modules/admin.js | 13 +- views/admin/create-restaurant.hbs | 2 +- views/admin/edit-restaurant.hbs | 2 +- views/admin/restaurant.hbs | 2 + views/partials/admin-restaurant-form.hbs | 9 +- 13 files changed, 415 insertions(+), 39 deletions(-) create mode 100644 helpers/file-helpers.js create mode 100644 middleware/multer.js create mode 100644 migrations/20230920130606-add-image-to-restaurants.js diff --git a/app.js b/app.js index 7a1ada2caa..4f0bf1c644 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,8 @@ +const path = require('path') const express = require('express') const handlebars = require('express-handlebars') // 引入 express-handlebars const flash = require('connect-flash') -const methodOverride = require("method-override"); +const methodOverride = require('method-override') const session = require('express-session') const passport = require('./config/passport') const handlebarsHelpers = require('./helpers/handlebars-helpers') @@ -20,7 +21,8 @@ app.use( app.use(passport.initialize()) app.use(passport.session()) app.use(flash()) -app.use(methodOverride("_method")); +app.use(methodOverride('_method')) +app.use('/upload', express.static(path.join(__dirname, 'upload'))) app.use((req, res, next) => { res.locals.success_messages = req.flash('success_messages') diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 3a44a3e6c1..6be3d8dbd5 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,4 +1,5 @@ const { Restaurant } = require('../models') +const { localFileHandler } = require("../helpers/file-helpers"); const adminController = { getRestaurants: (req, res, next) => { @@ -15,18 +16,24 @@ const adminController = { const { name, tel, address, openingHours, description } = req.body if (!name) throw new Error('Restaurant name is required!') - Restaurant.create({ - name, - tel, - address, - openingHours, - description - }) + const { file } = req; + + localFileHandler(file) + .then((filePath) => + Restaurant.create({ + name, + tel, + address, + openingHours, + description, + image: filePath || null, + }) + ) .then(() => { - req.flash('success_messages', 'restaurant was successfully created') - res.redirect('/admin/restaurants') + req.flash("success_messages", "restaurant was successfully created"); + res.redirect("/admin/restaurants"); }) - .catch(err => next(err)) + .catch((err) => next(err)); }, getRestaurant: (req, res, next) => { Restaurant.findByPk(req.params.id, { @@ -54,23 +61,26 @@ const adminController = { const { name, tel, address, openingHours, description } = req.body if (!name) throw new Error('Restaurant name is required!') - Restaurant.findByPk(req.params.id) - .then(restaurant => { - if (!restaurant) throw new Error("Restaurant didn't exist!") + const { file } = req; + + Promise.all([Restaurant.findByPk(req.params.id), localFileHandler(file)]) + .then(([restaurant, filePath]) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); return restaurant.update({ name, tel, address, openingHours, - description - }) + description, + image: filePath || restaurant.image + }); }) .then(() => { - req.flash('success_messages', 'restaurant was successfully to update') - res.redirect('/admin/restaurants') + req.flash("success_messages", "restaurant was successfully to update"); + res.redirect("/admin/restaurants"); }) - .catch(err => next(err)) + .catch((err) => next(err)); }, deleteRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id) diff --git a/helpers/file-helpers.js b/helpers/file-helpers.js new file mode 100644 index 0000000000..8af3d39aa4 --- /dev/null +++ b/helpers/file-helpers.js @@ -0,0 +1,18 @@ +const fs = require("fs"); + +const localFileHandler = (file) => { + return new Promise((resolve, reject) => { + if (!file) return resolve(null); + + const fileName = `upload/${file.originalname}`; + return fs.promises + .readFile(file.path) + .then((data) => fs.promises.writeFile(fileName, data)) + .then(() => resolve(`/${fileName}`)) + .catch((err) => reject(err)); + }); +}; + +module.exports = { + localFileHandler, +}; diff --git a/middleware/multer.js b/middleware/multer.js new file mode 100644 index 0000000000..fd9d306c52 --- /dev/null +++ b/middleware/multer.js @@ -0,0 +1,4 @@ +const multer = require("multer"); +const upload = multer({ dest: "temp/" }); + +module.exports = upload; diff --git a/migrations/20230920130606-add-image-to-restaurants.js b/migrations/20230920130606-add-image-to-restaurants.js new file mode 100644 index 0000000000..46186f88fd --- /dev/null +++ b/migrations/20230920130606-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('Restaurants', 'image') + } +} diff --git a/models/restaurant.js b/models/restaurant.js index dec7b0988a..08c157bba8 100644 --- a/models/restaurant.js +++ b/models/restaurant.js @@ -17,13 +17,14 @@ module.exports = (sequelize, DataTypes) => { tel: DataTypes.STRING, address: DataTypes.STRING, openingHours: DataTypes.STRING, - description: DataTypes.TEXT + description: DataTypes.TEXT, + image: DataTypes.STRING }, { sequelize, - modelName: 'Restaurant', - underscored: true + modelName: "Restaurant", + underscored: true, } - ) + ); return Restaurant } diff --git a/package-lock.json b/package-lock.json index aa6f008336..9588634266 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "express-handlebars": "^5.3.3", "express-session": "^1.17.2", "method-override": "^3.0.0", + "multer": "^1.4.3", "mysql2": "^2.3.0", "passport": "^0.4.1", "passport-local": "^1.0.0", @@ -443,6 +444,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -728,6 +734,23 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/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==" + }, + "node_modules/busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", + "dependencies": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1002,6 +1025,47 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "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" + } + }, + "node_modules/concat-stream/node_modules/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==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/config-chain": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", @@ -1074,6 +1138,11 @@ "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "dev": true }, + "node_modules/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==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1224,6 +1293,18 @@ "wrappy": "1" } }, + "node_modules/dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", + "dependencies": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -3106,8 +3187,7 @@ "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" }, "node_modules/isexe": { "version": "2.0.0", @@ -3455,9 +3535,12 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/mkdirp": { "version": "1.0.4", @@ -3834,6 +3917,36 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/multer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", + "integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", + "deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mysql2": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", @@ -4052,6 +4165,14 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -4420,6 +4541,11 @@ "node": ">=4" } }, + "node_modules/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==" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -4570,6 +4696,17 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5076,6 +5213,19 @@ "node": ">= 0.6" } }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, "node_modules/string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -5478,6 +5628,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -5619,6 +5774,11 @@ "node": ">=4" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5822,6 +5982,14 @@ "node": ">=8" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", @@ -6229,6 +6397,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": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -6450,6 +6623,20 @@ "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==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -6663,6 +6850,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": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "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", @@ -6720,6 +6947,11 @@ "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "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==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6840,6 +7072,15 @@ "wrappy": "1" } }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + } + }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -8240,8 +8481,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" }, "isexe": { "version": "2.0.0", @@ -8525,9 +8765,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "mkdirp": { "version": "1.0.4", @@ -8802,6 +9042,31 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", + "integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + } + } + }, "mysql2": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", @@ -8978,6 +9243,11 @@ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -9245,6 +9515,11 @@ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "dev": true }, + "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==" + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -9367,6 +9642,17 @@ } } }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -9734,6 +10020,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": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==" + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -10038,6 +10334,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -10146,6 +10447,11 @@ "prepend-http": "^2.0.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -10306,6 +10612,11 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, + "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", diff --git a/package.json b/package.json index a89387f838..06c6c2a6b8 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "express-handlebars": "^5.3.3", "express-session": "^1.17.2", "method-override": "^3.0.0", + "multer": "^1.4.3", "mysql2": "^2.3.0", "passport": "^0.4.1", "passport-local": "^1.0.0", diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 16db28ec74..1682a0159b 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -1,13 +1,22 @@ const express = require('express') const router = express.Router() const adminController = require('../../controllers/admin-controller') +const upload = require('../../middleware/multer') router.get('/restaurants/create', adminController.createRestaurant) router.get('/restaurants/:id/edit', adminController.editRestaurant) router.get('/restaurants/:id', adminController.getRestaurant) -router.put('/restaurants/:id', adminController.putRestaurant) +router.put( + '/restaurants/:id', + upload.single('image'), + adminController.putRestaurant +) router.delete('/restaurants/:id', adminController.deleteRestaurant) router.get('/restaurants', adminController.getRestaurants) -router.post('/restaurants', adminController.postRestaurant) +router.post( + '/restaurants', + upload.single('image'), + adminController.postRestaurant +) router.use('/', (req, res) => res.redirect('/admin/restaurants')) module.exports = router diff --git a/views/admin/create-restaurant.hbs b/views/admin/create-restaurant.hbs index a052c57a47..ca8cd4c28b 100644 --- a/views/admin/create-restaurant.hbs +++ b/views/admin/create-restaurant.hbs @@ -1,4 +1,4 @@ -
+

Create Restaurant

diff --git a/views/admin/edit-restaurant.hbs b/views/admin/edit-restaurant.hbs index 0d457971b8..8bb50a3828 100644 --- a/views/admin/edit-restaurant.hbs +++ b/views/admin/edit-restaurant.hbs @@ -1,4 +1,4 @@ - +

Edit Restaurant

diff --git a/views/admin/restaurant.hbs b/views/admin/restaurant.hbs index 249fea3875..275751d06d 100644 --- a/views/admin/restaurant.hbs +++ b/views/admin/restaurant.hbs @@ -4,6 +4,8 @@
+ +
  • Opening Hour: {{restaurant.openingHours}}
  • diff --git a/views/partials/admin-restaurant-form.hbs b/views/partials/admin-restaurant-form.hbs index 183a49db2e..c3bd0c2331 100644 --- a/views/partials/admin-restaurant-form.hbs +++ b/views/partials/admin-restaurant-form.hbs @@ -26,4 +26,11 @@
- \ No newline at end of file +
+ + +
+ + + +Back From 65bbbfcb4807b49e767ae89ae74f649e32ee9ad1 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Thu, 21 Sep 2023 08:22:05 +0800 Subject: [PATCH 23/38] feat: add seed data --- package-lock.json | 11 +++++ package.json | 1 + seeders/20230920142044-users-seed-file.js | 41 +++++++++++++++++++ .../20230921001737-restaurants-seed-file.js | 26 ++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 seeders/20230920142044-users-seed-file.js create mode 100644 seeders/20230921001737-restaurants-seed-file.js diff --git a/package-lock.json b/package-lock.json index 9588634266..45fd3fcf56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", + "faker": "^5.5.3", "method-override": "^3.0.0", "multer": "^1.4.3", "mysql2": "^2.3.0", @@ -2237,6 +2238,11 @@ "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" }, + "node_modules/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7804,6 +7810,11 @@ } } }, + "faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 06c6c2a6b8..7f6a7a830f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", + "faker": "^5.5.3", "method-override": "^3.0.0", "multer": "^1.4.3", "mysql2": "^2.3.0", diff --git a/seeders/20230920142044-users-seed-file.js b/seeders/20230920142044-users-seed-file.js new file mode 100644 index 0000000000..674cce76e8 --- /dev/null +++ b/seeders/20230920142044-users-seed-file.js @@ -0,0 +1,41 @@ +"use strict"; +const bcrypt = require("bcryptjs"); +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert( + "Users", + [ + { + // 一次新增三筆資料 + email: "root@example.com", + password: await bcrypt.hash("12345678", 10), + is_admin: true, + name: "root", + created_at: new Date(), + updated_at: new Date(), + }, + { + email: "user1@example.com", + password: await bcrypt.hash("12345678", 10), + is_admin: false, + name: "user1", + created_at: new Date(), + updated_at: new Date(), + }, + { + email: "user2@example.com", + password: await bcrypt.hash("12345678", 10), + is_admin: false, + name: "user2", + created_at: new Date(), + updated_at: new Date(), + }, + ], + {} + ); + }, + down: async (queryInterface, Sequelize) => { + // 清空資料表中所有資料 + await queryInterface.bulkDelete("Users", {}); + }, +}; diff --git a/seeders/20230921001737-restaurants-seed-file.js b/seeders/20230921001737-restaurants-seed-file.js new file mode 100644 index 0000000000..ee4d938eaa --- /dev/null +++ b/seeders/20230921001737-restaurants-seed-file.js @@ -0,0 +1,26 @@ +"use strict"; +const faker = require("faker"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert( + "Restaurants", + Array.from({ length: 50 }, () => ({ + 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(), + created_at: new Date(), + updated_at: new Date(), + })) + ); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete("Restaurants", null, {}); + }, +}; From 1e4a89fb37a594a313732150cc3140d2c7aef294 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Mon, 25 Sep 2023 19:25:09 +0800 Subject: [PATCH 24/38] feat: modify config for heroku --- Procfile | 1 + config/config.json | 6 +--- controllers/admin-controller.js | 28 +++++++++---------- middleware/multer.js | 6 ++-- .../20230921001737-restaurants-seed-file.js | 18 ++++++------ 5 files changed, 28 insertions(+), 31 deletions(-) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 0000000000..6feca7ecec --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: NODE_ENV=production node app.js \ No newline at end of file diff --git a/config/config.json b/config/config.json index 93cd6cd89d..a200ff75df 100644 --- a/config/config.json +++ b/config/config.json @@ -7,11 +7,7 @@ "dialect": "mysql" }, "production": { - "username": "root", - "password": null, - "database": "database_production", - "host": "127.0.0.1", - "dialect": "mysql" + "use_env_variable": "MYSQL_DATABASE_URL" }, "travis": { "username": "travis", diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 6be3d8dbd5..892b246bf8 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,5 +1,5 @@ const { Restaurant } = require('../models') -const { localFileHandler } = require("../helpers/file-helpers"); +const { localFileHandler } = require('../helpers/file-helpers') const adminController = { getRestaurants: (req, res, next) => { @@ -16,24 +16,24 @@ const adminController = { const { name, tel, address, openingHours, description } = req.body if (!name) throw new Error('Restaurant name is required!') - const { file } = req; + const { file } = req localFileHandler(file) - .then((filePath) => + .then(filePath => Restaurant.create({ name, tel, address, openingHours, description, - image: filePath || null, + image: filePath || null }) ) .then(() => { - req.flash("success_messages", "restaurant was successfully created"); - res.redirect("/admin/restaurants"); + req.flash('success_messages', 'restaurant was successfully created') + res.redirect('/admin/restaurants') }) - .catch((err) => next(err)); + .catch(err => next(err)) }, getRestaurant: (req, res, next) => { Restaurant.findByPk(req.params.id, { @@ -61,11 +61,11 @@ const adminController = { const { name, tel, address, openingHours, description } = req.body if (!name) throw new Error('Restaurant name is required!') - const { file } = req; + const { file } = req Promise.all([Restaurant.findByPk(req.params.id), localFileHandler(file)]) .then(([restaurant, filePath]) => { - if (!restaurant) throw new Error("Restaurant didn't exist!"); + if (!restaurant) throw new Error("Restaurant didn't exist!") return restaurant.update({ name, @@ -74,13 +74,13 @@ const adminController = { openingHours, description, image: filePath || restaurant.image - }); + }) }) .then(() => { - req.flash("success_messages", "restaurant was successfully to update"); - res.redirect("/admin/restaurants"); + req.flash('success_messages', 'restaurant was successfully to update') + res.redirect('/admin/restaurants') }) - .catch((err) => next(err)); + .catch(err => next(err)) }, deleteRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id) @@ -94,4 +94,4 @@ const adminController = { } } -module.exports = adminController; +module.exports = adminController diff --git a/middleware/multer.js b/middleware/multer.js index fd9d306c52..71fb05e07f 100644 --- a/middleware/multer.js +++ b/middleware/multer.js @@ -1,4 +1,4 @@ -const multer = require("multer"); -const upload = multer({ dest: "temp/" }); +const multer = require('multer') +const upload = multer({ dest: 'temp/' }) -module.exports = upload; +module.exports = upload diff --git a/seeders/20230921001737-restaurants-seed-file.js b/seeders/20230921001737-restaurants-seed-file.js index ee4d938eaa..4290b3fb2c 100644 --- a/seeders/20230921001737-restaurants-seed-file.js +++ b/seeders/20230921001737-restaurants-seed-file.js @@ -1,26 +1,26 @@ -"use strict"; -const faker = require("faker"); +'use strict' +const faker = require('faker') module.exports = { up: async (queryInterface, Sequelize) => { await queryInterface.bulkInsert( - "Restaurants", + 'Restaurants', Array.from({ length: 50 }, () => ({ name: faker.name.findName(), tel: faker.phone.phoneNumber(), address: faker.address.streetAddress(), - opening_hours: "08:00", + opening_hours: '08:00', image: `https://loremflickr.com/320/240/restaurant,food/?random=${ Math.random() * 100 }`, description: faker.lorem.text(), created_at: new Date(), - updated_at: new Date(), + updated_at: new Date() })) - ); + ) }, down: async (queryInterface, Sequelize) => { - await queryInterface.bulkDelete("Restaurants", null, {}); - }, -}; + await queryInterface.bulkDelete('Restaurants', null, {}) + } +} From d04bd47068883d2e90057a72012330a4173fb3a4 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Mon, 25 Sep 2023 22:34:58 +0800 Subject: [PATCH 25/38] feat: modify config for heroku --- seeders/20230920142044-users-seed-file.js | 40 +++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/seeders/20230920142044-users-seed-file.js b/seeders/20230920142044-users-seed-file.js index 674cce76e8..e226a40b9d 100644 --- a/seeders/20230920142044-users-seed-file.js +++ b/seeders/20230920142044-users-seed-file.js @@ -1,41 +1,41 @@ -"use strict"; -const bcrypt = require("bcryptjs"); +'use strict' +const bcrypt = require('bcryptjs') module.exports = { up: async (queryInterface, Sequelize) => { await queryInterface.bulkInsert( - "Users", + 'Users', [ { // 一次新增三筆資料 - email: "root@example.com", - password: await bcrypt.hash("12345678", 10), + email: 'root@example.com', + password: await bcrypt.hash('12345678', 10), is_admin: true, - name: "root", + name: 'root', created_at: new Date(), - updated_at: new Date(), + updated_at: new Date() }, { - email: "user1@example.com", - password: await bcrypt.hash("12345678", 10), + email: 'user1@example.com', + password: await bcrypt.hash('12345678', 10), is_admin: false, - name: "user1", + name: 'user1', created_at: new Date(), - updated_at: new Date(), + updated_at: new Date() }, { - email: "user2@example.com", - password: await bcrypt.hash("12345678", 10), + email: 'user2@example.com', + password: await bcrypt.hash('12345678', 10), is_admin: false, - name: "user2", + name: 'user2', created_at: new Date(), - updated_at: new Date(), - }, + updated_at: new Date() + } ], {} - ); + ) }, down: async (queryInterface, Sequelize) => { // 清空資料表中所有資料 - await queryInterface.bulkDelete("Users", {}); - }, -}; + await queryInterface.bulkDelete('Users', {}) + } +} From 69ec77c964576ca7f9b83ad4c5768b9fbbb904c3 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 26 Sep 2023 13:22:13 +0800 Subject: [PATCH 26/38] feat: modify config for heroku --- models/restaurant.js | 7 ++++--- models/user.js | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/models/restaurant.js b/models/restaurant.js index 08c157bba8..810e17917c 100644 --- a/models/restaurant.js +++ b/models/restaurant.js @@ -22,9 +22,10 @@ module.exports = (sequelize, DataTypes) => { }, { sequelize, - modelName: "Restaurant", - underscored: true, + modelName: 'Restaurant', + tableName: 'Restaurants', + underscored: true } - ); + ) return Restaurant } diff --git a/models/user.js b/models/user.js index 318cbffbc4..908a83ed2c 100644 --- a/models/user.js +++ b/models/user.js @@ -21,6 +21,7 @@ module.exports = (sequelize, DataTypes) => { { sequelize, modelName: 'User', + tableName: 'Users', underscored: true } ) From 823bef4d679e15932e918a02bedc7027697125fb Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Tue, 26 Sep 2023 23:10:19 +0800 Subject: [PATCH 27/38] feat: add imgur api --- .env.example | 1 + app.js | 4 + controllers/admin-controller.js | 28 +- helpers/file-helpers.js | 18 +- package-lock.json | 475 ++++++++++++++++++++++++++++++-- package.json | 2 + 6 files changed, 489 insertions(+), 39 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..80f596dc48 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +IMGUR_CLIENT_ID= \ No newline at end of file diff --git a/app.js b/app.js index 4f0bf1c644..ef6491b9e7 100644 --- a/app.js +++ b/app.js @@ -9,6 +9,10 @@ const handlebarsHelpers = require('./helpers/handlebars-helpers') const { getUser } = require('./helpers/auth-helpers') const routes = require('./routes') +if (process.env.NODE_ENV !== "production") { + require("dotenv").config(); +} + const app = express() const port = process.env.PORT || 3000 const SESSION_SECRET = 'secret' diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 892b246bf8..82f78b7eb1 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,5 +1,5 @@ const { Restaurant } = require('../models') -const { localFileHandler } = require('../helpers/file-helpers') +const { imgurFileHandler } = require("../helpers/file-helpers"); const adminController = { getRestaurants: (req, res, next) => { @@ -18,22 +18,22 @@ const adminController = { const { file } = req - localFileHandler(file) - .then(filePath => + imgurFileHandler(file) + .then((filePath) => Restaurant.create({ name, tel, address, openingHours, description, - image: filePath || null + image: filePath || null, }) ) .then(() => { - req.flash('success_messages', 'restaurant was successfully created') - res.redirect('/admin/restaurants') + req.flash("success_messages", "restaurant was successfully created"); + res.redirect("/admin/restaurants"); }) - .catch(err => next(err)) + .catch((err) => next(err)); }, getRestaurant: (req, res, next) => { Restaurant.findByPk(req.params.id, { @@ -63,9 +63,9 @@ const adminController = { const { file } = req - Promise.all([Restaurant.findByPk(req.params.id), localFileHandler(file)]) + Promise.all([Restaurant.findByPk(req.params.id), imgurFileHandler(file)]) .then(([restaurant, filePath]) => { - if (!restaurant) throw new Error("Restaurant didn't exist!") + if (!restaurant) throw new Error("Restaurant didn't exist!"); return restaurant.update({ name, @@ -73,14 +73,14 @@ const adminController = { address, openingHours, description, - image: filePath || restaurant.image - }) + image: filePath || restaurant.image, + }); }) .then(() => { - req.flash('success_messages', 'restaurant was successfully to update') - res.redirect('/admin/restaurants') + req.flash("success_messages", "restaurant was successfully to update"); + res.redirect("/admin/restaurants"); }) - .catch(err => next(err)) + .catch((err) => next(err)); }, deleteRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id) diff --git a/helpers/file-helpers.js b/helpers/file-helpers.js index 8af3d39aa4..1697898b76 100644 --- a/helpers/file-helpers.js +++ b/helpers/file-helpers.js @@ -1,4 +1,7 @@ const fs = require("fs"); +const imgur = require("imgur"); +const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID; +imgur.setClientId(IMGUR_CLIENT_ID); const localFileHandler = (file) => { return new Promise((resolve, reject) => { @@ -13,6 +16,19 @@ const localFileHandler = (file) => { }); }; +const imgurFileHandler = file => { + return new Promise((resolve, reject) => { + if (!file) return resolve(null) + + return imgur.uploadFile(file.path) + .then(img => { + resolve(img?.link || null) + }) + .catch(err => reject(err)) + }) +} + module.exports = { localFileHandler, -}; + imgurFileHandler +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 45fd3fcf56..b7eed95b56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "bcryptjs": "^2.4.3", "connect-flash": "^0.1.1", "dayjs": "^1.10.6", + "dotenv": "^10.0.0", "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", "faker": "^5.5.3", + "imgur": "^1.0.2", "method-override": "^3.0.0", "multer": "^1.4.3", "mysql2": "^2.3.0", @@ -256,6 +258,17 @@ "node": ">=6" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -264,12 +277,25 @@ "@types/ms": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", + "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -280,6 +306,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.5.tgz", "integrity": "sha512-H5Wn24s/ZOukBmDn03nnGTp18A60ny9AmCwnEcgJiTgSGsCO7k+NWP7zjCCbhlcnVCoI+co52dUAt9GMhOSULw==" }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/validator": { "version": "13.7.9", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.9.tgz", @@ -527,8 +561,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/balanced-match": { "version": "1.0.0", @@ -760,6 +793,14 @@ "node": ">= 0.8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, "node_modules/cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -980,7 +1021,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, "dependencies": { "mimic-response": "^1.0.0" } @@ -1002,7 +1042,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1258,7 +1297,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -1339,6 +1377,14 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/dottie": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", @@ -1408,7 +1454,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -2364,7 +2409,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -2739,8 +2783,7 @@ "node_modules/http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "node_modules/http-errors": { "version": "1.7.2", @@ -2757,6 +2800,18 @@ "node": ">= 0.6" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2783,6 +2838,188 @@ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", "dev": true }, + "node_modules/imgur": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/imgur/-/imgur-1.0.2.tgz", + "integrity": "sha512-bZJkRpa3ReR7lSEzAOjO4PPl9OIDQPuiKoG2aOh36PrTBQCrZL/oTcc6VClyyXEg9O6rEMpsuCloxfhqybpfZA==", + "dependencies": { + "commander": "^7.1.0", + "form-data": "^4.0.0", + "got": "^11.8.1" + }, + "bin": { + "imgur": "cli.js" + } + }, + "node_modules/imgur/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/imgur/node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/imgur/node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imgur/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/imgur/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imgur/node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/imgur/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imgur/node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/imgur/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/imgur/node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/imgur/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/imgur/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imgur/node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imgur/node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/imgur/node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3524,7 +3761,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, "engines": { "node": ">=4" } @@ -4604,7 +4840,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -4639,6 +4874,17 @@ "node": ">=0.6" } }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -4795,6 +5041,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6251,6 +6502,17 @@ "defer-to-connect": "^1.0.1" } }, + "@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -6259,12 +6521,25 @@ "@types/ms": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", + "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==" + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "requires": { + "@types/node": "*" + } + }, "@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -6275,6 +6550,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.5.tgz", "integrity": "sha512-H5Wn24s/ZOukBmDn03nnGTp18A60ny9AmCwnEcgJiTgSGsCO7k+NWP7zjCCbhlcnVCoI+co52dUAt9GMhOSULw==" }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/validator": { "version": "13.7.9", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.9.tgz", @@ -6467,8 +6750,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "balanced-match": { "version": "1.0.0", @@ -6648,6 +6930,11 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -6813,7 +7100,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, "requires": { "mimic-response": "^1.0.0" } @@ -6835,7 +7121,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -7050,8 +7335,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "denque": { "version": "2.1.0", @@ -7111,6 +7395,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", @@ -7173,7 +7462,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -7915,7 +8203,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8180,8 +8467,7 @@ "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "http-errors": { "version": "1.7.2", @@ -8195,6 +8481,15 @@ "toidentifier": "1.0.0" } }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8215,6 +8510,130 @@ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", "dev": true }, + "imgur": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/imgur/-/imgur-1.0.2.tgz", + "integrity": "sha512-bZJkRpa3ReR7lSEzAOjO4PPl9OIDQPuiKoG2aOh36PrTBQCrZL/oTcc6VClyyXEg9O6rEMpsuCloxfhqybpfZA==", + "requires": { + "commander": "^7.1.0", + "form-data": "^4.0.0", + "got": "^11.8.1" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "requires": { + "json-buffer": "3.0.1" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + } + } + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8764,8 +9183,7 @@ "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" }, "minimatch": { "version": "3.0.4", @@ -9577,7 +9995,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -9603,6 +10020,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -9722,6 +10144,11 @@ "path-parse": "^1.0.6" } }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/package.json b/package.json index 7f6a7a830f..a6f0e12b58 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,12 @@ "bcryptjs": "^2.4.3", "connect-flash": "^0.1.1", "dayjs": "^1.10.6", + "dotenv": "^10.0.0", "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", "faker": "^5.5.3", + "imgur": "^1.0.2", "method-override": "^3.0.0", "multer": "^1.4.3", "mysql2": "^2.3.0", From 9565fd2d4876cb6d090797d6d55fd057b25e0a17 Mon Sep 17 00:00:00 2001 From: Sunnylin0320 Date: Mon, 2 Oct 2023 22:18:59 +0800 Subject: [PATCH 28/38] R01 --- app.js | 17 +++++++++-- controllers/admin-controller.js | 53 +++++++++++++++++++++++++-------- middleware/auth.js | 8 ++--- routes/modules/admin.js | 4 +++ views/admin/users.hbs | 48 +++++++++++++++++++++++++++++ views/partials/header.hbs | 3 ++ 6 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 views/admin/users.hbs diff --git a/app.js b/app.js index ef6491b9e7..012df29358 100644 --- a/app.js +++ b/app.js @@ -9,14 +9,25 @@ const handlebarsHelpers = require('./helpers/handlebars-helpers') const { getUser } = require('./helpers/auth-helpers') const routes = require('./routes') -if (process.env.NODE_ENV !== "production") { - require("dotenv").config(); +if (process.env.NODE_ENV !== 'production') { + require('dotenv').config() } const app = express() const port = process.env.PORT || 3000 const SESSION_SECRET = 'secret' -app.engine('hbs', handlebars({ extname: '.hbs', helpers: handlebarsHelpers })) +app.engine( + 'hbs', + handlebars({ + extname: '.hbs', + helpers: { + ...handlebarsHelpers, + eq: function (a, b) { + return a === b + } + } + }) +) app.set('view engine', 'hbs') app.use(express.urlencoded({ extended: true })) app.use( diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 82f78b7eb1..92125e8382 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,5 +1,5 @@ -const { Restaurant } = require('../models') -const { imgurFileHandler } = require("../helpers/file-helpers"); +const { Restaurant, User } = require('../models') +const { imgurFileHandler } = require('../helpers/file-helpers') const adminController = { getRestaurants: (req, res, next) => { @@ -19,21 +19,21 @@ const adminController = { const { file } = req imgurFileHandler(file) - .then((filePath) => + .then(filePath => Restaurant.create({ name, tel, address, openingHours, description, - image: filePath || null, + image: filePath || null }) ) .then(() => { - req.flash("success_messages", "restaurant was successfully created"); - res.redirect("/admin/restaurants"); + req.flash('success_messages', 'restaurant was successfully created') + res.redirect('/admin/restaurants') }) - .catch((err) => next(err)); + .catch(err => next(err)) }, getRestaurant: (req, res, next) => { Restaurant.findByPk(req.params.id, { @@ -65,7 +65,7 @@ const adminController = { Promise.all([Restaurant.findByPk(req.params.id), imgurFileHandler(file)]) .then(([restaurant, filePath]) => { - if (!restaurant) throw new Error("Restaurant didn't exist!"); + if (!restaurant) throw new Error("Restaurant didn't exist!") return restaurant.update({ name, @@ -73,14 +73,14 @@ const adminController = { address, openingHours, description, - image: filePath || restaurant.image, - }); + image: filePath || restaurant.image + }) }) .then(() => { - req.flash("success_messages", "restaurant was successfully to update"); - res.redirect("/admin/restaurants"); + req.flash('success_messages', 'restaurant was successfully to update') + res.redirect('/admin/restaurants') }) - .catch((err) => next(err)); + .catch(err => next(err)) }, deleteRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id) @@ -91,6 +91,33 @@ const adminController = { }) .then(() => res.redirect('/admin/restaurants')) .catch(err => next(err)) + }, + getUsers: (req, res, next) => { + return User.findAll({ + raw: true + }) + .then(users => { + res.render('admin/users', { users }) + }) + .catch(err => next(err)) + }, + patchUser: (req, res, next) => { + return User.findByPk(req.params.id) + .then(user => { + if (user.email === 'root@example.com') { + req.flash('error_messages', '禁止變更 root 權限') + throw res.redirect('back') + } + return user + }) + .then(user => user.update({ isAdmin: !user.isAdmin })) + .then(() => { + req.flash('success_messages', '使用者權限變更成功') + res.redirect('/admin/users') + }) + .catch(err => { + next(err) + }) } } diff --git a/middleware/auth.js b/middleware/auth.js index 015c9a5bbe..83d4f45c22 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,8 +1,8 @@ -const { ensureAuthenticated, getUser } = require('../helpers/auth-helpers') +const helpers = require('../helpers/auth-helpers') const authenticated = (req, res, next) => { // if (req.isAuthenticated) - if (ensureAuthenticated(req)) { + if (helpers.ensureAuthenticated(req)) { return next() } res.redirect('/signin') @@ -10,8 +10,8 @@ const authenticated = (req, res, next) => { const authenticatedAdmin = (req, res, next) => { // if (req.isAuthenticated) - if (ensureAuthenticated(req)) { - if (getUser(req).isAdmin) return next() + if (helpers.ensureAuthenticated(req)) { + if (helpers.getUser(req).isAdmin) return next() res.redirect('/') } else { diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 1682a0159b..4ff1a626bd 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -12,6 +12,10 @@ router.put( adminController.putRestaurant ) router.delete('/restaurants/:id', adminController.deleteRestaurant) +// 修改使用者權限 +router.patch('/users/:id', adminController.patchUser) +// 顯示使用者清單 +router.get('/users', adminController.getUsers) router.get('/restaurants', adminController.getRestaurants) router.post( '/restaurants', diff --git a/views/admin/users.hbs b/views/admin/users.hbs new file mode 100644 index 0000000000..a098291279 --- /dev/null +++ b/views/admin/users.hbs @@ -0,0 +1,48 @@ +

餐廳後台

+ + + + + + + + + + + + + {{#each users}} + + + + + {{#if this.isAdmin}} + + + + + {{else}} + + + + + {{/if}} + + {{/each}} + +
#NameEmailRole#
{{this.id}}{{this.name}}{{this.email}}admin + + user
+ +
\ No newline at end of file diff --git a/views/partials/header.hbs b/views/partials/header.hbs index 3873900eca..c92986a938 100644 --- a/views/partials/header.hbs +++ b/views/partials/header.hbs @@ -13,6 +13,9 @@ + {{/if}}