# 第十二堂:JWT 身份驗證機制
- 錄影
- API 應用可參考[線稿圖](https://miro.com/app/board/uXjVLbiDml0=/)
- API 開發規格請依照 [此 API 文件](https://liberating-turtle-5a2.notion.site/38e4dd6775894e94a8f575f20ca5b867?v=17c6a246851881208205000cacf67220)
- db 欄位設計請依照 [此資料庫欄位圖片](https://firebasestorage.googleapis.com/v0/b/hexschool-courses.appspot.com/o/hex-website%2Fnode%2F1739179853094-fitness_5.png?alt=media&token=a65de209-3ae6-4263-bdc0-d4f08638ff20) 定義
## JWT 機制介紹
* [JWT 圖解](https://whimsical.com/jwt-UKUY1rj1vfoN6uyic7e4Sm@2Ux7TurymNEtgWEBxMsZ)、[官網介紹](https://jwt.io/)
* [base64 解碼](https://www.convertstring.com/zh_TW/EncodeDecode/Base64Decode)
```=javascript
<!-- 產生 JWT token:payload、secret、option -->
jwt.sign({id:user._id},secret,{
expiresIn: "90d"
})
// output:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYyNDk0NzUzMzI4OTMzMDM0NmYxMDkyNCIsImlhdCI6MTY0ODk2OTU1NSwiZXhwIjoxNzEyMDQxNTU1fQ.bjUj8QgQao6pgFE1MMzZeiD4q0d-VJN0a5CznJr4ZnE
<!-- 解密 JWT -->
jwt.verify(token, secretOrPublicKey, [options, callback])
// output
{
"id": "6249368f98f7f965568ad019",
"iat": 1648968184,
"exp": 1712040184
}
```
## 環境建立
1. 確認 env
2. 加入 NPM `jsonwebtoken`
3. 加入 config/**secret.js,並在 index.js 也增加**
```jsx=
module.exports = {
jwtSecret: process.env.JWT_SECRET,
jwtExpiresDay: process.env.JWT_EXPIRES_DAY
}
```
### utils/generateJWT.js
```jsx=
const jwt = require('jsonwebtoken')
/**
* create JSON Web Token
* @param {Object} payload token content
* @param {String} secret token secret
* @param {Object} [option] same to npm package - jsonwebtoken
* @returns {String}
*/
module.exports = (payload, secret, option = {}) => new Promise((resolve, reject) => {
jwt.sign(payload, secret, option, (err, token) => {
if (err) {
reject(err)
return
}
resolve(token)
})
})
```
### middlwares>auth.js
```jsx=
const jwt = require('jsonwebtoken')
const PERMISSION_DENIED_STATUS_CODE = 401
const FailedMessageMap = {
expired: 'Token 已過期',
invalid: '無效的 token',
missing: '請先登入'
}
function generateError (status, message) {
const error = new Error(message)
error.status = status
return error
}
function formatVerifyError (jwtError) {
let result
switch (jwtError.name) {
case 'TokenExpiredError':
result = generateError(PERMISSION_DENIED_STATUS_CODE, FailedMessageMap.expired)
break
default:
result = generateError(PERMISSION_DENIED_STATUS_CODE, FailedMessageMap.invalid)
break
}
return result
}
function verifyJWT (token, secret) {
return new Promise((resolve, reject) => {
jwt.verify(token, secret, (error, decoded) => {
if (error) {
reject(formatVerifyError(error))
} else {
resolve(decoded)
}
})
})
}
module.exports = ({
secret,
userRepository,
logger = console
}) => {
if (!secret || typeof secret !== 'string') {
logger.error('[AuthV2] secret is required and must be a string.')
throw new Error('[AuthV2] secret is required and must be a string.')
}
if (!userRepository || typeof userRepository !== 'object' || typeof userRepository.findOneBy !== 'function') {
logger.error('[AuthV2] userRepository is required and must be a function.')
throw new Error('[AuthV2] userRepository is required and must be a function.')
}
return async (req, res, next) => {
if (
!req.headers ||
!req.headers.authorization ||
!req.headers.authorization.startsWith('Bearer')
) {
logger.warn('[AuthV2] Missing authorization header.')
next(generateError(PERMISSION_DENIED_STATUS_CODE, FailedMessageMap.missing))
return
}
const [, token] = req.headers.authorization.split(' ')
if (!token) {
logger.warn('[AuthV2] Missing token.')
next(generateError(PERMISSION_DENIED_STATUS_CODE, FailedMessageMap.missing))
return
}
try {
const verifyResult = await verifyJWT(token, secret)
const user = await userRepository.findOneBy({ id: verifyResult.id })
if (!user) {
next(generateError(PERMISSION_DENIED_STATUS_CODE, FailedMessageMap.invalid))
return
}
req.user = user
next()
} catch (error) {
logger.error(`[AuthV2] ${error.message}`)
next(error)
}
}
}
```
### middlewares/isCoach.js
```jsx=
const FORBIDDEN_MESSAGE = '使用者尚未成為教練'
const PERMISSION_DENIED_STATUS_CODE = 401
function generateError (status = PERMISSION_DENIED_STATUS_CODE, message = FORBIDDEN_MESSAGE) {
const error = new Error(message)
error.status = status
return error
}
module.exports = (req, res, next) => {
if (!req.user || req.user.role !== 'COACH') {
next(generateError())
return
}
next()
}
```
### routes/user
```jsx=
const express = require('express')
const bcrypt = require('bcrypt')
const router = express.Router()
const config = require('../config/index')
const { dataSource } = require('../db/data-source')
const logger = require('../utils/logger')('Users')
const generateJWT = require('../utils/generateJWT')
const auth = require('../middlewares/auth')({
secret: config.get('secret').jwtSecret,
userRepository: dataSource.getRepository('User'),
logger
})
function isUndefined (value) {
return value === undefined
}
function isNotValidSting (value) {
return typeof value !== 'string' || value.trim().length === 0 || value === ''
}
router.post('/signup', async (req, res, next) => {
try {
const passwordPattern = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,16}/
const { name, email, password } = req.body
if (isUndefined(name) || isNotValidSting(name) || isUndefined(email) || isNotValidSting(email) || isUndefined(password) || isNotValidSting(password)) {
logger.warn('欄位未填寫正確')
res.status(400).json({
status: 'failed',
message: '欄位未填寫正確'
})
return
}
if (!passwordPattern.test(password)) {
logger.warn('建立使用者錯誤: 密碼不符合規則,需要包含英文數字大小寫,最短8個字,最長16個字')
res.status(400).json({
status: 'failed',
message: '密碼不符合規則,需要包含英文數字大小寫,最短8個字,最長16個字'
})
return
}
const userRepository = dataSource.getRepository('User')
const existingUser = await userRepository.findOne({
where: { email }
})
if (existingUser) {
logger.warn('建立使用者錯誤: Email 已被使用')
res.status(409).json({
status: 'failed',
message: 'Email 已被使用'
})
return
}
const salt = await bcrypt.genSalt(10)
const hashPassword = await bcrypt.hash(password, salt)
const newUser = userRepository.create({
name,
email,
role: 'USER',
password: hashPassword
})
const savedUser = await userRepository.save(newUser)
logger.info('新建立的使用者ID:', savedUser.id)
res.status(201).json({
status: 'success',
data: {
user: {
id: savedUser.id,
name: savedUser.name
}
}
})
} catch (error) {
logger.error('建立使用者錯誤:', error)
next(error)
}
})
router.post('/login', async (req, res, next) => {
try {
const passwordPattern = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,16}/
const { email, password } = req.body
if (isUndefined(email) || isNotValidSting(email) || isUndefined(password) || isNotValidSting(password)) {
logger.warn('欄位未填寫正確')
res.status(400).json({
status: 'failed',
message: '欄位未填寫正確'
})
return
}
if (!passwordPattern.test(password)) {
logger.warn('密碼不符合規則,需要包含英文數字大小寫,最短8個字,最長16個字')
res.status(400).json({
status: 'failed',
message: '密碼不符合規則,需要包含英文數字大小寫,最短8個字,最長16個字'
})
return
}
const userRepository = dataSource.getRepository('User')
const existingUser = await userRepository.findOne({
select: ['id', 'name', 'password'],
where: { email }
})
if (!existingUser) {
res.status(400).json({
status: 'failed',
message: '使用者不存在或密碼輸入錯誤'
})
return
}
logger.info(`使用者資料: ${JSON.stringify(existingUser)}`)
const isMatch = await bcrypt.compare(password, existingUser.password)
if (!isMatch) {
res.status(400).json({
status: 'failed',
message: '使用者不存在或密碼輸入錯誤'
})
return
}
const token = await generateJWT({
id: existingUser.id
}, config.get('secret.jwtSecret'), {
expiresIn: `${config.get('secret.jwtExpiresDay')}`
})
res.status(201).json({
status: 'success',
data: {
token,
user: {
name: existingUser.name
}
}
})
} catch (error) {
logger.error('登入錯誤:', error)
next(error)
}
})
router.get('/profile', auth, async (req, res, next) => {
try {
const { id } = req.user
const userRepository = dataSource.getRepository('User')
const user = await userRepository.findOne({
select: ['name', 'email'],
where: { id }
})
res.status(200).json({
status: 'success',
data: {
user
}
})
} catch (error) {
logger.error('取得使用者資料錯誤:', error)
next(error)
}
})
router.put('/profile', auth, async (req, res, next) => {
try {
const { id } = req.user
const { name } = req.body
if (isUndefined(name) || isNotValidSting(name)) {
logger.warn('欄位未填寫正確')
res.status(400).json({
status: 'failed',
message: '欄位未填寫正確'
})
return
}
const userRepository = dataSource.getRepository('User')
const user = await userRepository.findOne({
select: ['name'],
where: {
id
}
})
if (user.name === name) {
res.status(400).json({
status: 'failed',
message: '使用者名稱未變更'
})
return
}
const updatedResult = await userRepository.update({
id,
name: user.name
}, {
name
})
if (updatedResult.affected === 0) {
res.status(400).json({
status: 'failed',
message: '更新使用者資料失敗'
})
return
}
const result = await userRepository.findOne({
select: ['name'],
where: {
id
}
})
res.status(200).json({
status: 'success',
data: {
user: result
}
})
} catch (error) {
logger.error('取得使用者資料錯誤:', error)
next(error)
}
})
module.exports = router
```
## 中場休息
## routes/admin 教練身份加入登入條件
```jsx=
const express = require('express')
const router = express.Router()
const config = require('../config/index')
const { dataSource } = require('../db/data-source')
const logger = require('../utils/logger')('Admin')
const auth = require('../middlewares/auth')({
secret: config.get('secret').jwtSecret,
userRepository: dataSource.getRepository('User'),
logger
})
const isCoach = require('../middlewares/isCoach')
function isUndefined (value) {
return value === undefined
}
function isNotValidSting (value) {
return typeof value !== 'string' || value.trim().length === 0 || value === ''
}
function isNotValidInteger (value) {
return typeof value !== 'number' || value < 0 || value % 1 !== 0
}
router.post('/coaches/courses', auth, isCoach, async (req, res, next) => {
try {
const { id } = req.user
const {
skill_id: skillId, name, description, start_at: startAt, end_at: endAt,
max_participants: maxParticipants, meeting_url: meetingUrl
} = req.body
if (isUndefined(skillId) || isNotValidSting(skillId) ||
isUndefined(name) || isNotValidSting(name) ||
isUndefined(description) || isNotValidSting(description) ||
isUndefined(startAt) || isNotValidSting(startAt) ||
isUndefined(endAt) || isNotValidSting(endAt) ||
isUndefined(maxParticipants) || isNotValidInteger(maxParticipants) ||
isUndefined(meetingUrl) || isNotValidSting(meetingUrl) || !meetingUrl.startsWith('https')) {
logger.warn('欄位未填寫正確')
res.status(400).json({
status: 'failed',
message: '欄位未填寫正確'
})
return
}
const courseRepo = dataSource.getRepository('Course')
const newCourse = courseRepo.create({
user_id: id,
skill_id: skillId,
name,
description,
start_at: startAt,
end_at: endAt,
max_participants: maxParticipants,
meeting_url: meetingUrl
})
const savedCourse = await courseRepo.save(newCourse)
const course = await courseRepo.findOne({
where: { id: savedCourse.id }
})
res.status(201).json({
status: 'success',
data: {
course
}
})
} catch (error) {
logger.error(error)
next(error)
}
})
router.put('/coaches/courses/:courseId', auth, isCoach, async (req, res, next) => {
try {
const { id } = req.user
const { courseId } = req.params
const {
skill_id: skillId, name, description, start_at: startAt, end_at: endAt,
max_participants: maxParticipants, meeting_url: meetingUrl
} = req.body
if (isNotValidSting(courseId) ||
isUndefined(skillId) || isNotValidSting(skillId) ||
isUndefined(name) || isNotValidSting(name) ||
isUndefined(description) || isNotValidSting(description) ||
isUndefined(startAt) || isNotValidSting(startAt) ||
isUndefined(endAt) || isNotValidSting(endAt) ||
isUndefined(maxParticipants) || isNotValidInteger(maxParticipants) ||
isUndefined(meetingUrl) || isNotValidSting(meetingUrl) || !meetingUrl.startsWith('https')) {
logger.warn('欄位未填寫正確')
res.status(400).json({
status: 'failed',
message: '欄位未填寫正確'
})
return
}
const courseRepo = dataSource.getRepository('Course')
const existingCourse = await courseRepo.findOne({
where: { id: courseId, user_id: id }
})
if (!existingCourse) {
logger.warn('課程不存在')
res.status(400).json({
status: 'failed',
message: '課程不存在'
})
return
}
const updateCourse = await courseRepo.update({
id: courseId
}, {
skill_id: skillId,
name,
description,
start_at: startAt,
end_at: endAt,
max_participants: maxParticipants,
meeting_url: meetingUrl
})
if (updateCourse.affected === 0) {
logger.warn('更新課程失敗')
res.status(400).json({
status: 'failed',
message: '更新課程失敗'
})
return
}
const savedCourse = await courseRepo.findOne({
where: { id: courseId }
})
res.status(200).json({
status: 'success',
data: {
course: savedCourse
}
})
} catch (error) {
logger.error(error)
next(error)
}
})
router.post('/coaches/:userId', async (req, res, next) => {
try {
const { userId } = req.params
const { experience_years: experienceYears, description, profile_image_url: profileImageUrl = null } = req.body
if (isUndefined(experienceYears) || isNotValidInteger(experienceYears) || isUndefined(description) || isNotValidSting(description)) {
logger.warn('欄位未填寫正確')
res.status(400).json({
status: 'failed',
message: '欄位未填寫正確'
})
return
}
if (profileImageUrl && !isNotValidSting(profileImageUrl) && !profileImageUrl.startsWith('https')) {
logger.warn('大頭貼網址錯誤')
res.status(400).json({
status: 'failed',
message: '欄位未填寫正確'
})
return
}
const userRepository = dataSource.getRepository('User')
const existingUser = await userRepository.findOne({
select: ['id', 'name', 'role'],
where: { id: userId }
})
if (!existingUser) {
logger.warn('使用者不存在')
res.status(400).json({
status: 'failed',
message: '使用者不存在'
})
return
} else if (existingUser.role === 'COACH') {
logger.warn('使用者已經是教練')
res.status(409).json({
status: 'failed',
message: '使用者已經是教練'
})
return
}
const coachRepo = dataSource.getRepository('Coach')
const newCoach = coachRepo.create({
user_id: userId,
experience_years: experienceYears,
description,
profile_image_url: profileImageUrl
})
const updatedUser = await userRepository.update({
id: userId,
role: 'USER'
}, {
role: 'COACH'
})
if (updatedUser.affected === 0) {
logger.warn('更新使用者失敗')
res.status(400).json({
status: 'failed',
message: '更新使用者失敗'
})
return
}
const savedCoach = await coachRepo.save(newCoach)
const savedUser = await userRepository.findOne({
select: ['name', 'role'],
where: { id: userId }
})
res.status(201).json({
status: 'success',
data: {
user: savedUser,
coach: savedCoach
}
})
} catch (error) {
logger.error(error)
next(error)
}
})
module.exports = router
```
## 新增課程
### entities/CreditPurchase.js
```jsx=
const { EntitySchema } = require('typeorm')
module.exports = new EntitySchema({
name: 'CreditPurchase',
tableName: 'CREDIT_PURCHASE',
columns: {
id: {
primary: true,
type: 'uuid',
generated: 'uuid',
nullable: false
},
user_id: {
type: 'uuid',
nullable: false
},
credit_package_id: {
type: 'uuid',
nullable: false
},
purchased_credits: {
type: 'integer',
nullable: false
},
price_paid: {
type: 'numeric',
precision: 10,
scale: 2,
nullable: false
},
createdAt: {
type: 'timestamp',
createDate: true,
name: 'created_at',
nullable: false
},
purchaseAt: {
type: 'timestamp',
name: 'purchase_at',
nullable: false
}
},
relations: {
User: {
target: 'User',
type: 'many-to-one',
joinColumn: {
name: 'user_id',
referencedColumnName: 'id',
foreignKeyConstraintName: 'credit_purchase_user_id_fk'
}
},
CreditPackage: {
target: 'CreditPackage',
type: 'many-to-one',
joinColumn: {
name: 'credit_package_id',
referencedColumnName: 'id',
foreignKeyConstraintName: 'credit_purchase_credit_package_id_fk'
}
}
}
})
```
### entities/CourseBooking.js
```jsx=
const { EntitySchema } = require('typeorm')
module.exports = new EntitySchema({
name: 'CourseBooking',
tableName: 'COURSE_BOOKING',
columns: {
id: {
primary: true,
type: 'uuid',
generated: 'uuid',
nullable: false
},
user_id: {
type: 'uuid',
nullable: false
},
course_id: {
type: 'uuid',
nullable: false
},
bookingAt: {
type: 'timestamp',
createDate: true,
name: 'booking_at',
nullable: false
},
joinAt: {
type: 'timestamp',
name: 'join_at',
nullable: true
},
leaveAt: {
type: 'timestamp',
name: 'leave_at',
nullable: true
},
cancelledAt: {
type: 'timestamp',
name: 'cancelled_at',
nullable: true
},
cancellation_reason: {
type: 'varchar',
length: 255,
nullable: true
},
createdAt: {
type: 'timestamp',
createDate: true,
name: 'created_at',
nullable: false
}
},
relations: {
User: {
target: 'User',
type: 'many-to-one',
joinColumn: {
name: 'user_id',
referencedColumnName: 'id',
foreignKeyConstraintName: 'course_booking_user_id_fk'
}
},
CreditPackage: {
target: 'Course',
type: 'many-to-one',
joinColumn: {
name: 'course_id',
referencedColumnName: 'id',
foreignKeyConstraintName: 'course_booking_course_id_fk'
}
}
}
})
```
> 加入到 db/datasource.js
### routes/creditPackage.js
```jsx=
const express = require('express')
const router = express.Router()
const config = require('../config/index')
const { dataSource } = require('../db/data-source')
const logger = require('../utils/logger')('CreditPackage')
const auth = require('../middlewares/auth')({
secret: config.get('secret').jwtSecret,
userRepository: dataSource.getRepository('User'),
logger
})
function isUndefined (value) {
return value === undefined
}
function isNotValidSting (value) {
return typeof value !== 'string' || value.trim().length === 0 || value === ''
}
function isNotValidInteger (value) {
return typeof value !== 'number' || value < 0 || value % 1 !== 0
}
router.get('/', async (req, res, next) => {
try {
const creditPackage = await dataSource.getRepository('CreditPackage').find({
select: ['id', 'name', 'credit_amount', 'price']
})
res.status(200).json({
status: 'success',
data: creditPackage
})
} catch (error) {
logger.error(error)
next(error)
}
})
router.post('/', async (req, res, next) => {
try {
const { name, credit_amount: creditAmount, price } = req.body
if (isUndefined(name) || isNotValidSting(name) ||
isUndefined(creditAmount) || isNotValidInteger(creditAmount) ||
isUndefined(price) || isNotValidInteger(price)) {
res.status(400).json({
status: 'failed',
message: '欄位未填寫正確'
})
return
}
const creditPackageRepo = dataSource.getRepository('CreditPackage')
const existCreditPackage = await creditPackageRepo.find({
where: {
name
}
})
if (existCreditPackage.length > 0) {
res.status(409).json({
status: 'failed',
message: '資料重複'
})
return
}
const newCreditPurchase = await creditPackageRepo.create({
name,
credit_amount: creditAmount,
price
})
const result = await creditPackageRepo.save(newCreditPurchase)
res.status(200).json({
status: 'success',
data: result
})
} catch (error) {
logger.error(error)
next(error)
}
})
router.post('/:creditPackageId', auth, async (req, res, next) => {
try {
const { id } = req.user
const { creditPackageId } = req.params
const creditPackageRepo = dataSource.getRepository('CreditPackage')
const creditPackage = await creditPackageRepo.findOne({
where: {
id: creditPackageId
}
})
if (!creditPackage) {
res.status(400).json({
status: 'failed',
message: 'ID錯誤'
})
return
}
const creditPurchaseRepo = dataSource.getRepository('CreditPurchase')
const newPurchase = await creditPurchaseRepo.create({
user_id: id,
credit_package_id: creditPackageId,
purchased_credits: creditPackage.credit_amount,
price_paid: creditPackage.price,
purchaseAt: new Date().toISOString()
})
await creditPurchaseRepo.save(newPurchase)
res.status(200).json({
status: 'success',
data: null
})
} catch (error) {
logger.error(error)
next(error)
}
})
router.delete('/:creditPackageId', async (req, res, next) => {
try {
const { creditPackageId } = req.params
if (isUndefined(creditPackageId) || isNotValidSting(creditPackageId)) {
res.status(400).json({
status: 'failed',
message: '欄位未填寫正確'
})
return
}
const result = await dataSource.getRepository('CreditPackage').delete(creditPackageId)
if (result.affected === 0) {
res.status(400).json({
status: 'failed',
message: 'ID錯誤'
})
return
}
res.status(200).json({
status: 'success',
data: result
})
} catch (error) {
logger.error(error)
next(error)
}
})
module.exports = router
```
### routes/courses.js
```jsx=
const express = require('express')
const { IsNull } = require('typeorm')
const router = express.Router()
const config = require('../config/index')
const { dataSource } = require('../db/data-source')
const logger = require('../utils/logger')('Course')
const auth = require('../middlewares/auth')({
secret: config.get('secret').jwtSecret,
userRepository: dataSource.getRepository('User'),
logger
})
router.get('/', async (req, res, next) => {
try {
const courses = await dataSource.getRepository('Course').find({
select: {
id: true,
name: true,
description: true,
start_at: true,
end_at: true,
max_participants: true,
User: {
name: true
},
Skill: {
name: true
}
},
relations: {
User: true,
Skill: true
}
})
res.status(200).json({
status: 'success',
data: courses.map((course) => {
return {
id: course.id,
name: course.name,
description: course.description,
start_at: course.start_at,
end_at: course.end_at,
max_participants: course.max_participants,
coach_name: course.User.name,
skill_name: course.Skill.name
}
})
})
} catch (error) {
logger.error(error)
next(error)
}
})
router.post('/:courseId', auth, async (req, res, next) => {
try {
const { id } = req.user
const { courseId } = req.params
const courseRepo = dataSource.getRepository('Course')
const course = await courseRepo.findOne({
where: {
id: courseId
}
})
if (!course) {
res.status(400).json({
status: 'failed',
message: 'ID錯誤'
})
return
}
const creditPurchaseRepo = dataSource.getRepository('CreditPurchase')
const courseBookingRepo = dataSource.getRepository('CourseBooking')
const userCourseBooking = await courseBookingRepo.findOne({
where: {
user_id: id,
course_id: courseId
}
})
if (userCourseBooking) {
res.status(400).json({
status: 'failed',
message: '已經報名過此課程'
})
return
}
const userCredit = await creditPurchaseRepo.sum('purchased_credits', {
user_id: id
})
const userUsedCredit = await courseBookingRepo.count({
where: {
user_id: id,
cancelledAt: IsNull()
}
})
const courseBookingCount = await courseBookingRepo.count({
where: {
course_id: courseId,
cancelledAt: IsNull()
}
})
if (userUsedCredit >= userCredit) {
res.status(400).json({
status: 'failed',
message: '已無可使用堂數'
})
return
} else if (courseBookingCount >= course.max_participants) {
res.status(400).json({
status: 'failed',
message: '已達最大參加人數,無法參加'
})
return
}
const newCourseBooking = await courseBookingRepo.create({
user_id: id,
course_id: courseId
})
await courseBookingRepo.save(newCourseBooking)
res.status(201).json({
status: 'success',
data: null
})
} catch (error) {
logger.error(error)
next(error)
}
})
router.delete('/:courseId', auth, async (req, res, next) => {
try {
const { id } = req.user
const { courseId } = req.params
const courseBookingRepo = dataSource.getRepository('CourseBooking')
const userCourseBooking = await courseBookingRepo.findOne({
where: {
user_id: id,
course_id: courseId,
cancelledAt: IsNull()
}
})
if (!userCourseBooking) {
res.status(400).json({
status: 'failed',
message: 'ID錯誤'
})
return
}
const updateResult = await courseBookingRepo.update(
{
user_id: id,
course_id: courseId,
cancelledAt: IsNull()
},
{
cancelledAt: new Date().toISOString()
}
)
if (updateResult.affected === 0) {
res.status(400).json({
status: 'failed',
message: '取消失敗'
})
return
}
res.status(200).json({
status: 'success',
data: null
})
} catch (error) {
logger.error(error)
next(error)
}
})
module.exports = router
```