Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 30 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
const path = require('path')
const express = require('express')
const handlebars = require('express-handlebars')
const methodOverride = require('method-override')
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')

if (process.env.NODE_ENV !== 'production') {
require('dotenv').config()
}
const app = express()
const port = process.env.PORT || 3000
const SESSION_SECRET = 'secret'

// 註冊 Handlebars 樣板引擎,並指定副檔名為 .hbs
app.engine('hbs', handlebars({ extname: '.hbs', helpers: handlebarsHelpers }))
// 設定使用 Handlebars 做為樣板引擎
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(methodOverride('_method'))
// 因為是靜態檔案,所以不需要像其他的路由一樣寫 controller 邏輯,直接用 express.static 來指定路徑就可以了
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()
})
app.use(routes)

app.listen(port, () => {
Expand Down
6 changes: 1 addition & 5 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 Down
35 changes: 35 additions & 0 deletions config/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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) => {
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 => {
return cb(null, user)
})
})
module.exports = passport
114 changes: 114 additions & 0 deletions controllers/admin-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const { Restaurant, User } = require('../models')
const { imgurFileHandler } = require('../helpers/file-helpers')
const adminController = {
getRestaurants: (req, res, next) => {
return Restaurant.findAll({
raw: true
})
.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 // 從 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 這筆餐廳資料
name,
tel,
address,
openingHours,
description,
image: filePath || null
}))
.then(() => {
req.flash('success_messages', 'restaurant was successfully created')
res.redirect('/admin/restaurants')
})
.catch(err => next(err))
},
getRestaurant: (req, res, next) => {
return 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))
},
editRestaurant: (req, res, next) => {
return 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!')
// NOTE: 這一段沒有設定 { raw: true } 因為這邊會需要 update
// 編輯情境裡是不會加 { raw: true }!!
const { file } = req // 把檔案取出來
return 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 (使用者沒有上傳新照片) 就沿用原本資料庫內的值
})
})
.then(() => {
req.flash('success_messages', 'restaurant was successfully to update')
res.redirect('/admin/restaurants')
})
.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!")
return restaurant.destroy()
})
.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 權限')
res.redirect('back')
} else {
user.update({
isAdmin: !user.isAdmin
})
req.flash('success_messages', '使用者權限變更成功')
}
})
.then(() => res.redirect('/admin/users'))
.catch(err => next(err))
}
}

module.exports = adminController
6 changes: 6 additions & 0 deletions controllers/restaurant-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const restaurantController = {
getRestaurants: (req, res) => {
return res.render('restaurants')
}
}
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') // 載入 bcrypt
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
10 changes: 10 additions & 0 deletions helpers/auth-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const getUser = req => {
return req.user || null
}
const ensureAuthenticated = req => {
return req.isAuthenticated()
}
module.exports = {
getUser,
ensureAuthenticated
}
28 changes: 28 additions & 0 deletions helpers/file-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const fs = require('fs') // 引入 fs 模組
const imgur = require('imgur')
const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID
imgur.setClientId(IMGUR_CLIENT_ID)
const localFileHandler = file => { // file 是 multer 處理完的檔案
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))
})
}
const imgurFileHandler = file => {
return new Promise((resolve, reject) => {
if (!file) return resolve(null)
return imgur.uploadFile(file.path)
.then(img => {
resolve(img?.link || null) // 檢查 img 是否存在
})
.catch(err => reject(err))
})
}
module.exports = {
localFileHandler,
imgurFileHandler
}
4 changes: 4 additions & 0 deletions helpers/handlebars-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const dayjs = require('dayjs') // 載入 dayjs 套件
module.exports = {
currentYear: () => dayjs().year() // 取得當年年份作為 currentYear 的屬性值,並導出
}
Loading