Skip to content
Open

R03-Yen #2041

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fca7f3b
feat: add R01.test.js and unit-test-helpers.js
Carrot7712 Jan 13, 2022
96da4be
feat:add R02.test.js
Carrot7712 Jan 13, 2022
ef44c3e
add R03.test.js file
AmberYen Mar 2, 2022
4fe60b0
correcting typo in comment
zjzheng17 Jul 18, 2022
3c8c792
feat: add handlebars
freeanser Aug 2, 2023
b926952
feat: create main.hbs and render restaurants.hbs
freeanser Aug 2, 2023
d75d82b
feat: add admin controller
freeanser Aug 2, 2023
27345b6
feat: create signup
freeanser Aug 3, 2023
c4979e6
feat: flash msg & signup verification
freeanser Aug 3, 2023
5744b62
feat: passport init & signin
freeanser Aug 4, 2023
0f81e21
feat: add header & footer
freeanser Aug 4, 2023
135d8c3
feat: user model add is_admin
freeanser Aug 5, 2023
5ebe5ae
feat: add restaurant model
freeanser Aug 5, 2023
77d7d3d
feat: modify admin restaurants page
freeanser Aug 5, 2023
6d07c71
feat: create restaurant
freeanser Aug 6, 2023
9c2c8d2
feat: admin restaurant page
freeanser Aug 6, 2023
2acab73
feat: admin update restaurant
freeanser Aug 6, 2023
50a8a4e
feat: delete restaurant
freeanser Aug 6, 2023
5477c42
feat: add seed data
freeanser Aug 7, 2023
a631075
feat: modify config for heroku
freeanser Aug 11, 2023
d9090e4
feat: add imgur api
freeanser Aug 24, 2023
ec5a9bd
feat: User permission management
freeanser Aug 28, 2023
9dd39ce
fix conflicts
freeanser Aug 31, 2023
2bc8f2a
feat: User permission management
freeanser Aug 28, 2023
8155b20
Squashed commit of the following:
YCS1015 Sep 21, 2023
68f191f
modify create page
freeanser Sep 22, 2023
9cb7232
feat: modify config for heroku
freeanser Sep 27, 2023
aafbba9
feat: User permission management
freeanser Oct 12, 2023
d5d0d6f
R02 test: add dashboard
freeanser Oct 12, 2023
c681a83
fix conflicts
freeanser Oct 12, 2023
dcc5e47
feat: modify config for heroku
freeanser Oct 12, 2023
bd73c24
fix conflict
freeanser Oct 13, 2023
d60cdc3
feat: add categories navbar on restaurants index page
freeanser Oct 13, 2023
890af02
feat: add pagination on restaurants index page
freeanser Oct 15, 2023
c7717fb
feat: add comment model
freeanser Oct 17, 2023
379275b
feat: add comment model
freeanser Oct 17, 2023
c37e1b3
feat: show comments on restaurant page
freeanser Oct 17, 2023
a43f0c2
add delete comment for admin
freeanser Oct 17, 2023
6b681a0
feat: add user profile
freeanser Oct 22, 2023
8c449c6
fix conflict
freeanser Oct 22, 2023
50ee422
feat: add comments on profile page
freeanser Oct 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
IMGUR_CLIENT_ID=
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: NODE_ENV=production node app.js

30 changes: 28 additions & 2 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
const path = require('path') // temp 資料夾
const express = require('express')
const handlebars = require('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')
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'

app.engine('hbs', handlebars({ extname: '.hbs', helpers: handlebarsHelpers })) // 放入 helpers 的位置
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()) // 初始化 Passport
app.use(passport.session()) // 啟動 session 功能
app.use(flash())
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') // 設定 success_msg 訊息
res.locals.error_messages = req.flash('error_messages') // 設定 warning_msg 訊息
res.locals.user = getUser(req)
next() // 往 middleware
})
app.use(routes)

app.listen(port, () => {
console.info(`Example app listening on port ${port}!`)
console.info(`Express is running on http://localhost:${port}`)
})

module.exports = app
8 changes: 2 additions & 6 deletions config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -20,4 +16,4 @@
"dialect": "mysql",
"logging": false
}
}
}
38 changes: 38 additions & 0 deletions config/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const passport = require('passport')
const LocalStrategy = require('passport-local')
const bcrypt = require('bcryptjs')
const db = require('../models')
const User = db.User
// set up Passport strategy
passport.use(new LocalStrategy(
// customize user field
{
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
},
// authenticate user
(req, email, password, cb) => {
// cb 對應到官方文件裡的 done。這裡講師習慣寫成 cb,也就是在驗證後要執行的另一個 callback function
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 => {
// Sequalize 打包後的物件,多包裝了幾層,並且裡面加上一些 Sequalize 內建的參數與方法,讓我們可以直接透過 Sequalize 操作這筆資料,例如刪除或更新 user,不過我們的專案不會用到這些功能,所以就可以運用 toJSON() 這個小技巧來整理格式,把資料簡化變更為我們比較容易取用的樣子。
user = user.toJSON()
return cb(null, user)
})
})
module.exports = passport
135 changes: 135 additions & 0 deletions controllers/admin-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const { Restaurant, User, Category } = require('../models')
// const { localFileHandler } = require('../helpers/file-helpers')
const { imgurFileHandler } = require('../helpers/file-helpers')

const adminController = {
getRestaurants: (req, res) => {
Restaurant.findAll({
raw: true,
nest: true,
include: [Category]
})
.then(restaurants => {
return res.render('admin/restaurants', { restaurants: restaurants })
})
},
createRestaurant: (req, res, next) => {
return Category.findAll({
raw: true
})
.then(categories => res.render('admin/create-restaurant', { categories }))
.catch(err => next(err))
},
postRestaurant: (req, res, next) => {
const { name, tel, address, openingHours, description, categoryId } = req.body // 從 req.body 拿出表單裡的資料
if (!name) throw new Error('Restaurant name is required!') // name 是必填,若發先是空值就會終止程式碼,並在畫面顯示錯誤提示
const { file } = req // 把檔案取出來,也可以寫成 const file = req.file
return imgurFileHandler(file) // 把取出的檔案傳給 file-helper 處理後
.then(filePath => Restaurant.create({ // 再 create 這筆餐廳資料,產生一個新的 Restaurant 物件實例,並存入資料庫
name,
tel,
address,
openingHours,
description,
image: filePath || null,
// image: filePath || null 的意思是:如果 filePath 的值為檔案路徑字串(使用者有上傳檔案,就會被判斷為 Truthy),就將 image 的值設為檔案路徑;如果 filePath 的值是空的(也就是沒有上傳檔案,因此沒有檔案路徑,會被判斷為 Falsy),那麼就將 image 的值設為 null
categoryId
}))
.then(() => {
req.flash('success_messages', 'restaurant was successfully created')
res.redirect('/admin/restaurants') // 存完之後,回首頁
})
.catch(err => next(err))
},
// getRestaurant 而不是 getRestaurants,代表要 show specific restaurant
getRestaurant: (req, res, next) => {
Restaurant.findByPk(req.params.id, { // 去資料庫用 id 找一筆資料
raw: true,
nest: true,
include: [Category]
})
.then(restaurant => {
if (!restaurant) throw new Error("Restaurant didn't exist!") // 如果找不到,回傳錯誤訊息,後面不執行
res.render('admin/restaurant', { restaurant })
})
.catch(err => next(err))
},
editRestaurant: (req, res, next) => {
return Promise.all([
Restaurant.findByPk(req.params.id, { raw: true }),
Category.findAll({ raw: true })
])
.then(([restaurant, categories]) => {
if (!restaurant) throw new Error("Restaurant didn't exist!")
res.render('admin/edit-restaurant', { restaurant, categories })
})
.catch(err => next(err))
},
putRestaurant: (req, res, next) => {
// 拿資料、更新資料、閃flash、回restaurants清單
const { name, tel, address, openingHours, description, categoryId } = req.body
if (!name) throw new Error('Restaurant name is required!')
const { file } = req // 把檔案取出來
Promise.all([ // 非同步處理
Restaurant.findByPk(req.params.id), // 去資料庫查有沒有這間餐廳
imgurFileHandler(file) // 把檔案傳到 file-helper 處理
])
.then(([restaurant, filePath]) => { // 以上兩樣事都做完以後
if (!restaurant) throw new Error("Restaurant didn't exist!")
return restaurant.update({ // 修改這筆資料
name,
tel,
address,
openingHours,
description,
image: filePath || restaurant.image, // 如果 filePath 是 Truthy (使用者有上傳新照片) 就用 filePath,是 Falsy (使用者沒有上傳新照片) 就沿用原本資料庫內的值
categoryId
})
})
.then(() => {
req.flash('success_messages', 'restaurant was successfully to update')
res.redirect('/admin/restaurants')
})
.catch(err => next(err))
// 沒有設定 { raw: true } 來整理成乾淨的資料,這是因為這邊會需要用到 restaurant.update 這個方法,如果加上參數就會把 sequelize 提供的這個方法過濾掉,會無法使用。因此在編輯情境裡我們是不會加 { raw: true } 的
},
deleteRestaurant: (req, res, next) => {
return Restaurant.findByPk(req.params.id)
.then(restaurant => {
if (!restaurant) throw new Error("Restaurant didn't exist!")
return restaurant.destroy()
// 刪除的時候也不會加 { raw: true } 參數
})
.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) => {
const userId = req.params.id
// const { email } = req.body // 這是用在有 post / put 時
return User.findByPk(userId)
.then(user => {
// root@example.com
if (user.email === 'root@example.com') {
req.flash('error_messages', '禁止變更 root 權限')
return res.redirect('back')

// 不是 root@example.com 的其他
} else {
return user.update({
isAdmin: !user.isAdmin // 在 user 中要改變的key(所以不用 user.isAdmin): 要改成什麼 value (要確認是什麼資訊,所以要 user.isAdmin)
})
.then(() => {
req.flash('success_messages', '使用者權限變更成功')
return res.redirect('/admin/users')
}) // .then() 可以接住 return 的資料
}
})
.catch(err => next(err))
}
}
module.exports = adminController
50 changes: 50 additions & 0 deletions controllers/category-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const { Category } = require('../models')

const categoryController = {
getCategories: (req, res, next) => {
return Promise.all([
Category.findAll({ raw: true }),
req.params.cid ? Category.findByPk(req.params.cid, { raw: true }) : null
])
.then(([categories, category]) => {
// if (!category) throw Error("Category doesn't exist!") // 不需要這行,因為category可以不存在
return res.render('admin/categories', { categories, category })
}
)
.catch(err => next(err))
},
postCategory: (req, res, next) => {
const { name } = req.body
if (!name) throw new Error('Category name is required!')
return Category.create({ name })
.then(() => res.redirect('/admin/categories'))
.catch(err => next(err))
},
putCategory: (req, res, next) => {
// find the date that user wants to update
const { name } = req.body
// if no value => warn
if (!name) throw new Error('Category name is required!')
return Category.findByPk(req.params.cid)
// update
.then(category => {
if (!category) throw new Error("Category doesn't exist!")
return category.update({ name })
})
// redirect
.then(() => res.redirect('/admin/categories'))
.catch(err => next(err))
},
deleteCategory: (req, res, next) => {
return Category.findByPk(req.params.id)
.then(category => {
if (!category) throw new Error("Category didn't exist!")
return category.destroy()
})
.then(() => res.redirect('/admin/categories'))
.catch(err => next(err))
}

}

module.exports = categoryController
82 changes: 82 additions & 0 deletions controllers/restaurant-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const { Restaurant, Category, Comment, User } = require('../models')
const { getOffset, getPagination } = require('../helpers/pagination-helper')

const restaurantController = {
getRestaurants: (req, res, next) => {
const DEFAULT_LIMIT = 9
// req.query.categoryId是指會自動尋找 ? categoryId = 的值,也就是 ?categoryId={{this.id}}
// req.query.categoryId 返回字串
const categoryId = Number(req.query.categoryId) || ''
const page = Number(req.query.page) || 1
const limit = Number(req.query.limit) || DEFAULT_LIMIT
const offset = getOffset(limit, page)

return Promise.all([
// Sequelize 的 findAndCountAll,這個方法會在撈資料時一併計算資料量,此時回傳結果會有兩個部分:
// count - 資料量
// rows - 餐廳資料集合
Restaurant.findAndCountAll({
include: Category,
where: {
...categoryId ? { categoryId } : {}
},
limit,
offset,
nest: true,
raw: true
}),
Category.findAll({
raw: true
})
])
.then(([restaurants, categories]) => {
const data = restaurants.rows.map(r => ({
...r,
description: r.description.substring(0, 50)
}))
return res.render('restaurants', {
restaurants: data,
categories,
categoryId,
pagination: getPagination(limit, page, restaurants.count)
})
})
.catch(err => next(err))
},
getRestaurant: (req, res, next) => {
return Restaurant.findByPk(req.params.id, {
include: [

Category,
{
model: Comment, include: User
}

],
nest: true
// raw: true
})
.then(async restaurant => {
if (!restaurant) throw new Error("Restaurant didn't exist!")
await restaurant.increment('viewCounts', { by: 1 })
res.render('restaurant', { restaurant: restaurant.toJSON() })
})
.catch(err => next(err))
},
getDashboard: (req, res, next) => {
// get specific restaurant and include Category
return Restaurant.findByPk(req.params.id, {
include: Category,
nest: true,
raw: true
})
// render dashboard
.then(restaurant => {
if (!restaurant) throw new Error("Restaurant didn't exist!")
res.render('dashboard', { restaurant })
})
.catch(err => next(err))
}
}

module.exports = restaurantController
Loading