Skip to content
Open

R02-Yen #2033

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 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
7ece79e
add R01.test.js && unit-test-helper.js
AmberYen Mar 2, 2022
b54a2e9
add R02.test.js file
AmberYen Mar 2, 2022
79ff158
註解 typo 修正
zjzheng17 Jul 18, 2022
9d566b6
correcting typo in comment
zjzheng17 Jul 18, 2022
03c14c0
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
59d371b
fix conflict
freeanser Sep 30, 2023
a98bfed
feat: add category model
freeanser Sep 30, 2023
1369847
feat: update seed files
freeanser Oct 1, 2023
ee1323f
feat: show category on admin restaurant pages
freeanser Oct 1, 2023
1f96f87
feat: add categories selector on admin create and edit page
freeanser Oct 1, 2023
c118c4f
feat: add admin categories page
freeanser Oct 2, 2023
88a2671
feat: category create
freeanser Oct 3, 2023
b4bc8bd
feat: category update
freeanser Oct 6, 2023
0b7540d
feat: category delete
freeanser Oct 6, 2023
742692a
feat: add restaurants index page
freeanser Oct 6, 2023
d3d0253
Merge remote-tracking branch 'origin/R01-test' into R02
freeanser Oct 6, 2023
471a00c
feat: add restaurant page
freeanser Oct 6, 2023
8a7e022
R02 test: add dashboard
freeanser Oct 12, 2023
c260d5b
fix R01.test
freeanser Oct 12, 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=
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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
47 changes: 47 additions & 0 deletions controllers/restaurant-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { Restaurant, Category } = require('../models')

const restaurantController = {
getRestaurants: (req, res, next) => {
return Restaurant.findAll({
include: Category,
nest: true,
raw: true
})
.then(restaurants => {
const data = restaurants.map(r => ({
...r,
description: r.description.substring(0, 50)
}))
return res.render('restaurants', { restaurants: data })
})
},
getRestaurant: (req, res, next) => {
return Restaurant.findByPk(req.params.id, {
include: Category,
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
42 changes: 42 additions & 0 deletions controllers/user-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const bcrypt = require('bcryptjs')
const db = require('../models')
const { User } = db
const userController = {
signUpPage: (req, res) => {
res.render('signup')
},
signUp: (req, res, next) => {
// 如果兩次輸入的密碼不同,就建立一個 Error 物件並拋出
if (req.body.password !== req.body.passwordCheck) throw new Error('Passwords do not match!')

// 確認資料裡面沒有一樣的 email,若有,就建立一個 Error 物件並拋出
User.findOne({ where: { email: req.body.email } })
.then(user => {
if (user) throw new Error('Email already exists!')
return bcrypt.hash(req.body.password, 10) // 前面加 return
})
.then(hash => User.create({ // 上面錯誤狀況都沒發生,就把使用者的資料寫入資料庫
name: req.body.name,
email: req.body.email,
password: hash
}))
.then(() => {
req.flash('success_messages', '成功註冊帳號!') // 並顯示成功訊息
res.redirect('/signin')
})
.catch(err => next(err)) // 接住前面拋出的錯誤,呼叫專門做錯誤處理的 middleware
},
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
Loading