Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7ece79e
add R01.test.js && unit-test-helper.js
AmberYen Mar 2, 2022
79ff158
註解 typo 修正
zjzheng17 Jul 18, 2022
03c14c0
correcting typo in comment
zjzheng17 Jul 18, 2022
d5b1553
chore: init github workflow
eugenechen0514 Sep 9, 2023
fe01b7f
Update unit-test-helper.js
tuterwell Sep 12, 2023
e2665da
chore: format code
eugenechen0514 Sep 17, 2023
1be932b
test: fix update method
eugenechen0514 Sep 17, 2023
d12a942
Merge commit '1be932b9d1998168679d2cdf5a7a9e2f1d2bca91' into R01-test
eugenechen0514 Sep 17, 2023
fa173a7
chore: fix code format
eugenechen0514 Sep 17, 2023
1b8cd24
Delete .travis.yml
tuterwell Sep 18, 2023
d6e54dd
feat: add handlebars
allenliao0119 Aug 16, 2024
b9fb6f8
feat: add index page
allenliao0119 Aug 16, 2024
13cd3ee
feat: add admin index page
allenliao0119 Aug 17, 2024
6d99fec
feat: add user model
allenliao0119 Aug 18, 2024
8aa3822
feat: user signup
allenliao0119 Aug 18, 2024
7c0a0a7
feat: flash msg & signup verification
allenliao0119 Aug 18, 2024
e54130e
feat: passport init & signin
allenliao0119 Aug 19, 2024
2fa78fd
feat: add header & footer
allenliao0119 Aug 20, 2024
26fc420
feat: user model add is_admin
allenliao0119 Aug 20, 2024
559d3b1
feat: add restaurant model
allenliao0119 Aug 20, 2024
e7c600c
feat: modify admin restaurants page
allenliao0119 Aug 20, 2024
6b12f40
feat: create restaurant
allenliao0119 Aug 20, 2024
48032d9
feat: admin restaurant page
allenliao0119 Aug 21, 2024
29d16ee
feat: admin update restaurant
allenliao0119 Aug 21, 2024
fc50350
feat: delete restaurant
allenliao0119 Aug 21, 2024
454ff02
feat: add restaurant image
allenliao0119 Aug 22, 2024
1e6e02c
feat: add seed data
allenliao0119 Aug 23, 2024
70462c5
fix conflict
allenliao0119 Aug 23, 2024
f3e77cc
feat: modify user seed data to fit test requirement
allenliao0119 Aug 23, 2024
875c180
feat: adjust middlewares/auth.js
allenliao0119 Aug 23, 2024
100c0da
feat: add admin users page
allenliao0119 Aug 23, 2024
acb7f4d
fix conflict
allenliao0119 Aug 23, 2024
2b46d83
feat: add category model
allenliao0119 Aug 23, 2024
170333b
feat: update seed files
allenliao0119 Aug 24, 2024
840139d
feat: show category on admin restaurant pages
allenliao0119 Aug 24, 2024
f01342d
feat: add ifEqual hbs helper and update category selector
allenliao0119 Aug 25, 2024
c53dadb
feat: implement admin categories page and create category
allenliao0119 Aug 25, 2024
ace3fba
feat: category delete
allenliao0119 Aug 26, 2024
8d1df04
feat: add restaurants index page
allenliao0119 Aug 26, 2024
4d5439e
feat: add restaurant page
allenliao0119 Aug 26, 2024
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
26 changes: 26 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
const express = require('express')
const handlebars = require('express-handlebars')
const handlebarsHelper = require('./helpers/handlebars-helpers')
const flash = require('connect-flash')
const session = require('express-session')
const methodOverride = require('method-override')
const passport = require('passport')
const { getUser } = require('./helpers/auth-helpers')
const routes = require('./routes')
const path = require('path')

const app = express()
const port = process.env.PORT || 3000
const SESSION_SECRET = 'secret'

app.engine('hbs', handlebars({ extname: '.hbs', helpers: handlebarsHelper }))
app.set('view engine', 'hbs')

app.use(methodOverride('_method'))
app.use('/upload', express.static(path.join(__dirname, 'upload')))
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) => {
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)

Expand Down
38 changes: 38 additions & 0 deletions config/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const bcrypt = require('bcryptjs')
const passport = require('passport')
const LocalStrategy = require('passport-local')

const db = require('../models')
const User = db.User

passport.use(new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
},
(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(isMatched => {
if (!isMatched) return cb(null, false, req.flash('error_messages', '帳號或密碼輸入錯誤!'))
return cb(null, user)
})
})
.catch(err => cb(err))
}
))

passport.serializeUser((user, cb) => {
return cb(null, user.id)
})

passport.deserializeUser((id, cb) => {
User.findByPk(id).then(user => {
user = user.toJSON()
return cb(null, user)
})
})

module.exports = passport
132 changes: 132 additions & 0 deletions controllers/admin-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const { Restaurant, User, Category } = require('../models')

const { LocalFileHandler } = require('../helpers/file-helpers')

const adminController = {
getRestaurants: (req, res, next) => {
return Restaurant.findAll({
raw: true,
nest: true,
include: [Category]
})
.then(restaurants => {
return res.render('admin/restaurants', { restaurants })
})
.catch(err => next(err))
},
createRestaurant: (req, res) => {
return Category.findAll({ raw: true })
.then(categories => res.render('admin/create-restaurant', { categories }))
},
postRestaurant: (req, res, next) => {
const { name, tel, address, openingHours, description, categoryId } = req.body
if (!name) throw new Error('Restaurant name is required!')
const { file } = req
LocalFileHandler(file)
.then(filePath => {
return Restaurant.create({
name,
tel,
address,
openingHours,
description,
image: filePath || null,
categoryId
})
})
.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,
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) => {
const { name, tel, address, openingHours, description, categoryId } = req.body
if (!name) throw new Error('Restaurant name is required!')
const { file } = req
return 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,
image: filePath || restaurant.image,
categoryId
})
})
.then(() => {
req.flash('success_messages', 'restaurant was successfully updated')
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(() => {
req.flash('success_messages', 'restaurant was successfully deleted')
res.redirect('/admin/restaurants')
})
.catch(err => next(err))
},
getUsers: (req, res, next) => {
return User.findAll({
attributes: ['id', 'name', 'email', 'isAdmin'],
raw: true
})
.then(users => res.render('admin/users', { users }))
.catch(err => next(err))
},
patchUser: (req, res, next) => {
return User.findOne({
attributes: ['id', 'email', 'isAdmin'],
where: { id: req.params.id }
})
.then(user => {
if (!user) throw new Error("User didn't exist!")
if (user.email === 'root@example.com') {
req.flash('error_messages', '禁止變更 root 權限')
return res.redirect('back')
}
return user.update({ isAdmin: !user.isAdmin }, { where: { id: user.id } })
})
.then(() => {
req.flash('success_messages', '使用者權限變更成功')
res.redirect('/admin/users')
})
.catch(err => next(err))
}
}
module.exports = adminController
49 changes: 49 additions & 0 deletions controllers/category-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { Category } = require('../models')

const categoryController = {
getCategories: (req, res, next) => {
Promise.all([
Category.findAll({ raw: true }),
req.params.id ? Category.findByPk(req.params.id, { raw: true }) : null
])
.then(([categories, category]) => { 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(() => {
req.flash('success_messages', 'category was successfully created')
res.redirect('/admin/categories')
})
.catch(err => next(err))
},
putCategory: (req, res, next) => {
const { name } = req.body
if (!name) throw new Error('category name is required')
Category.findByPk(req.params.id)
.then(category => {
if (!category) throw new Error("category didn't exist")
return category.update({ name })
})
.then(() => {
req.flash('success_messages', 'category was successfully updated')
res.redirect('/admin/categories')
})
.catch(err => next(err))
},
deleteCategory: (req, res, next) => {
Category.findByPk(req.params.id)
.then(category => {
if (!category) throw new Error("category didn't exist")
return category.destroy()
})
.then(() => {
req.flash('success_messages', 'category and related restaurants were successfully deleted')
res.redirect('/admin/categories')
})
.catch(err => next(err))
}
}
module.exports = categoryController
34 changes: 34 additions & 0 deletions controllers/restaurant-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { restart } = require('nodemon')
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(restaurant => {
return {
...restaurant,
description: restaurant.description.substring(0, 50)
}
})
res.render('restaurants', { restaurants: data })
})
.catch(err => next(err))
},
getRestaurant: (req, res, next) => {
Restaurant.findByPk(req.params.id, {
include: [Category],
nest: true,
raw: true
})
.then(restaurant => {
if (!restaurant) throw new Error("restaurant didn't exist")
res.render('restaurant', { restaurant })
})
.catch(err => next(err))
}
}
module.exports = restaurantController
43 changes: 43 additions & 0 deletions controllers/user-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const bcrypt = require('bcryptjs')
const db = require('../models')
const { User } = db
const userController = {
signUpPage: (req, res) => {
return res.render('signup')
},
signUp: (req, res, next) => {
// 如果兩次輸入密碼不同,建立Error物件並拋出去
if (req.body.password !== req.body.passwordCheck) throw new Error('Passwords do not match!')
return User.findOne({ where: { email: req.body.email } })
.then(user => {
// 如果email已存在,建立Error物件並拋出去
if (user) throw new Error('Email already exists!')
return bcrypt.hash(req.body.password, 10)
})
.then(hash => User.create({
name: req.body.name,
email: req.body.email,
password: hash
}))
.then(() => {
req.flash('success_messages', '成功註冊帳號!')
res.redirect('/signin')
})
// 接住前面拋出來的錯誤,呼叫專門做錯誤處理的middlewares
.catch(error => next(error))
},
signInPage: (req, res) => {
res.render('signin')
},
signIn: (req, res) => {
req.flash('success_messages', '成功登入!')
res.redirect('/restaurants')
},
signOut: (req, res) => {
req.flash('success_messages', '登出成功!')
req.logout()
res.redirect('/signin')
}
}

module.exports = userController
11 changes: 11 additions & 0 deletions helpers/auth-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const getUser = req => {
return req.user || null
}
const ensureAuthenticated = req => {
return req.isAuthenticated()
}

module.exports = {
getUser,
ensureAuthenticated
}
14 changes: 14 additions & 0 deletions helpers/file-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const fs = require('fs')
const LocalFileHandler = file => {
return new Promise((resolve, reject) => {
if (!file) return resolve(null)
const fileName = `upload/${file.originalname}`
fs.promises.readFile(file.path)
.then(data => fs.promises.writeFile(fileName, data))
.then(() => resolve(`/${fileName}`))
.catch(err => reject(err))
})
}
module.exports = {
LocalFileHandler
}
13 changes: 13 additions & 0 deletions helpers/handlebars-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const dayjs = require('dayjs')
module.exports = {
currentYear: () => dayjs().year(),
role: isAdmin => {
return isAdmin ? 'admin' : 'user'
},
toggle: boolean => {
return !boolean
},
ifEqual: function (a, b, options) {
return a === b ? options.fn(this) : options.inverse(this)
}
}
Loading