# Express
###### tags: `Nodejs`
Express是基於nodejs寫的一個框架,它將很多功能都包裝得更好用、大幅增加了開發速度

## API
API可以被當成是一個程式之間溝通的媒介,例如要使用node下的fs或http等功能就要透過node API來進行操作、操作DOM元素也是要透過javascript的DOM API來進行操作

## RESTfulAPI
### 設計標準

1. 將API以資源方式區分

例如跟tours相關的資料就放在tours裡
2. 以資源命名的url

3. 以http method來區分不同用法
例如我addTours的功能和deleteTours的功能都在/tours的路徑
用post方法訪問/tours時就會觸發addTours
用delete方法訪問/tours時就會觸發deleteTours
4. 以JSend形式傳送資料
一般的JSON長這樣

建議傳送出去前先處理成這樣再傳(JSend),將資料包在data物件中並帶有一個狀態碼,收資料的一方會比較好處裡
```javascript=
// 大概就像這樣包一下再傳
const tours = JSON.parse(fs.readFileSync(`${__dirname}/dev-data/data/tours-simple.json`))
app.get('/api/v1/tours', (req, res) => {
res.status(200).json({
status: 'success',
data: tours
})
})
```

5. Stateless

狀態部分一律在client端處理(目前頁數、登入狀態),後端只負責送資料,不需要去記錄狀態
可以將不必要負荷分給client端
以翻頁為例

### 路由配置
express可以依照http method來配置路由
1. 簡單配置
```javascript=
const express = require('express')
cosnt app = express()
//get
app.get('/', (req, res) => {
//.......... do something
})
// post
app.post('/', (req, res) => {
//.......... do something
})
// others.....
```
2. 進階配置
```javascript=
app
.route('/api/v1/tours')
.get(getAllTours)
.post(addTour)
app
.route('/api/v1/tours/:id')
.get(getTour)
.patch(updateTour)
.delete(deleteTour)
```
3. 進階配置(整理版)
```javascript=
const tourRouter = express.Router()
app.use('/api/v1/tours', tourRouter) // 取出根路徑另外存到變數裡
tourRouter // 使用該變數就能使用訪問該route
.route('/') // 訪問根路徑/api/v1/tours
.get(getAllTours) // 對應路徑功能
.post(addTour)
tourRouter
.route('/:id') // 訪問/api/v1/tours/:id路徑
.get(getTour)
.patch(updateTour)
.delete(deleteTour)
```
4. 對不存在的route做處理
```javascript=
// **other route methods**
// 不存在的處理要放在最後
app.all('*', (req, res) => {
res.status(404).json({
state: 'failed',
message: `route ${req.originalUrl} doesn't exist`
})
})
```
### 路由參數
```javascript=
app.get('/api/v1/tours/:id/:x?', (req, res) => { // 後面補一個?代表選填 這裡的x代表選填參數
console.log(req.params) // 取得路由參數
res.status(200).json({
status: 'success',
data: tours[req.params.id]
})
})
```
## Middleware
### 簡介
[官網middleware頁面](https://expressjs.com/en/resources/middleware.html)

request和response物件在server端做處理的一連串過程中會經過的關卡(例如驗證使用者、body parser之類的功能等)就叫做middleware,middleware的執行順序就是看他們在code中的順序決定,需透過next()前往下一個步驟,千萬別忘記next(),如果沒有next()會把整個程式卡在middleware
### 實際演練
1. 自定義middleware
通常會透過app.use()來使用middleware
例如很熟悉的body parser功能
```javascript=
const express = require('express')
const app = express()
app.use(express.json()) // 使用express.json()這個middleware (body parser)
app.use((req, res, next) => { // middleware的寫法示範
console.log('my middleware')
next()
})
app.use((req, res, next) => {
req.requestTime = new Date().toISOString() // 加上時間並轉成好看的格式
console.log(req.requestTime)
next()
})
```
2. 3-rd party middleware
使用npm安裝完第三方套件並引入就能使用了
以morgan這個middleware套件為例
```javascript=
const express = require('express')
const morgan = require('morgan')
const app = express()
app.use(morgan('dev'))
```
3. param middleware
在一些需要用ID找資料的行為中,會需要先驗證ID是否存在,這時候可能會寫這樣
```javascript=
const getTour = (req, res) => {
if (req.params.id > tours.length) {
return res.status(404).json({
status: 'failed',
data: 'invalid ID'
})
}
// do something
}
router
.route('/:id').get(getTour)
```
但在很多操作都要驗證ID時還這樣寫的話就會變這樣
每個動作都有一個重複的檢查程式碼
<font color='red'>這樣非常不好維護</font>
```javascript=
const getTour = (req, res) => {
if (req.params.id > tours.length) {
return res.status(404).json({
status: 'failed',
data: 'invalid ID'
})
}
// do something
}
const addTour = (req, res) => {
if (req.params.id > tours.length) {
return res.status(404).json({
status: 'failed',
data: 'invalid ID'
})
}
// do something
}
const updateTour = (req, res) => {
if (req.params.id > tours.length) {
return res.status(404).json({
status: 'failed',
data: 'invalid ID'
})
}
// do something
}
const deletTour = (req, res) => {
if (req.params.id > tours.length) {
return res.status(404).json({
status: 'failed',
data: 'invalid ID'
})
}
// do something
}
router
.route('/:id')
.get(getTour)
.post(addTour)
.update(updateTour)
.delete(deletTour)
```
這時候router.param()就能派上用場了
這個用法可以傳入參數及middleware來使用
```javascript=
const checkID = (req,res,next,val) => {
if (val > tours.length) { // val會自動代入傳入的參數,也就是param的第一個參數(這裡為'id')
return res.status(404).json({
status: 'failed',
data: 'invalid ID'
})
}
next()
}
router.param('id', checkID) // 這樣每個需要透過id訪問的route都會先經過checkID的檢查
router
.route('/:id')
.get(getTour)
.post(addTour)
.update(updateTour)
.delete(deletTour)
```
4. 特定方法middleware
如果需要在post加一個只給他用的middleware就這樣加
通常用於新增內容的驗證
```javascript=
router
.route('/:id')
.get(getTour)
.post(middleware, addTour) // 使用addTour前會先經過middleware
```
### 訪問靜態檔案
```javascript=
app.use(express.static('路徑'))
// 用根路徑/該路徑下的檔名就能訪問該路徑的檔案
// 例如public底下有overview.html檔
app.use(express.static(`${__dirname}/public`))
// 之後用127.0.0.1:8000/overview.html就能看到該html頁
```
## 環境變數
Windows環境先安裝這個套件可以更方便地使用環境變數
```
npm install -g win-node-env
```
開發環境和使用者使用的環境會有所不同,開發者會用到很多使用者用不到的功能,而環境變數就是拿來區隔不同環境用的,例如使用者和開發者會連接到不同的資料庫、使用者和開發者會用不同的路徑抓資料
先將環境變數定義在config.env檔案中
要使用環境變數有兩個簡單步驟
1. npm i dotenv
2. 引入該套件
```javascript=
// server.js
const dotenv = require('dotenv')
dotenv.config({path: './config.env'}) // config.env的路徑
console.log(process.env) // 能看到在config.env中定義的資料被存進去了
```
### 使用環境變數的值
```javascript=
const port = process.env.PORT || 3000
app.listen(port, '127.0.0.1', () => {
console.log(`listening to http://127.0.0.1:${port}/`)
})
```
### 特定環境使用
```javascript=
if(process.env.NODE_ENV === 'development'){ // 只有在 development環境下用這個middleware
app.use(morgan('dev'))
}
```
### npm script啟用特定環境
```
"scripts": {
"start": "nodemon server.js", // 預設為啟用development模式
"prod": "NODE_ENV=production nodemon server.js" // 啟用production模式
}
```
<font color='red'>使用windows要先安裝開頭題到的套件才能直接在npm script設定NODE_ENV=XXX</font>
## MVC架構
MVC就是Model、View、Controller的縮寫
1. Model: 掌管資料及業務邏輯(Business logic)
2. View: 視圖層(例如app的介面、網頁頁面)
3. Controller: 處理與Model互動的request並送response給client
### 運作模式
1. client端送request通過router觸發特定的controller方法
2. controller依方法跟model要資料
3. 要到資料後送給view
4. view將資料填入網頁模板
5. view將填好的網頁送給controller
6. controller發送將網頁response回client

#### Controller(Application logic)

#### Model(Business logic)

#### 設計要點: Fat model thin controller
盡量將複雜的邏輯放在Model,讓controller可以擔任好橋樑的角色
## 錯誤處理
程式的錯誤通常分為
1. 作業錯誤Operational error: <br>作業程序上的錯誤,通常指route錯誤、輸入資料錯誤、伺服器連接失敗、連接超時等可以被預料到的錯誤<br>
2. 程式錯誤Programming error: <br>程式碼上的錯誤,這類比較偏向程式碼沒寫好導致的錯誤,例如常見的read property of undefined、錯誤資料類型、傳錯參數等程式碼上的錯誤<br>
錯誤處理要處理的錯誤都是Operational error類型的,因為這方面的錯誤通常可以因client端達成正確行為而被改善,在express中可以透過一個middleware來處理所有這方面的錯誤並送response回去通知使用者出錯原因,透過獨立的middleware可以讓錯誤處理獨立出來不影響到其他程式碼

### 建立錯誤處理middleware
複習一下middleware都是透過app.use()來呼叫的,而middleware是照順序執行,所以這個錯誤處理middleware必須放在middleware的最後一個步驟
```javascript=
// server.js
// **other middlewares**
//
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500
err.status = err.status || 'error'
res.status(err.statusCode).json({
state: err.status,
message: err.message
})
})
```
再來測試一下
```javascript=
// 建立一個攔截為定義route的router
app.all('*', (req, res, next) => {
// 建立error物件
const error = new Error(`route ${req.originalUrl} doesn't exist`)
// (Error()內的字串就是error message)
error.statusCode = 404
error.status = 'fail'
next(error) // 帶有error的next()會自動將error帶到下一個middleware
})
```
執行結果
成功將error物件帶入middleware中response回client端
```
{
"state": "fail",
"message": "route /api/v1/toursasdasd doesn't exist"
}
```
#### 為error及error middleware建立一個獨立class
1. apiError.js
```javascript=
class ApiError extends Error {
constructor(message, statusCode) {
super(message) // 使用super以使用母class(Error)的方法
this.statusCode = statusCode
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'
this.isOperational = true
Error.captureStackTrace(this, this.constructor)
}
}
module.exports = ApiError
```
2. errorController.js
```javascript=
module.exports = (err, req, res, next) => {
err.statusCode = err.statusCode || 500
err.status = err.status || 'error'
res.status(err.statusCode).json({
state: err.status,
message: err.message
})
}
```
將這兩個檔案import並整合到前面的程式碼
```javascript=
const apiError = require('./utils/apiError')
const errorController = require('./controller/errorController')
app.all('*', (req, res, next) => {
next(new apiError(`route ${req.originalUrl} doesn't exist`, 404))
// 以apiError為原型建立error物件
})
app.use(errorController)
```
#### handling error in async functions
#### 加密
```
npm i bcryptjs
```
```javascript=
// userModel.js
userSchema.pre('save', async function(next){
if(!this.isModified('password'))return next()
this.password = await bcrypt.hash(this.password, 12)
next()
})
```
## 驗證
驗證是一個非常重要的步驟
### 驗證方法
由於REST的設計標準,我們需要保持一個stateless的狀態,有別於於傳統的將登入狀態存在伺服器中,這裡會介紹JWT(Json Web Token)的驗證方法,
### 驗證的運作模式
1. client端登入(將帳號密碼透過request傳給server)
2. server檢查該組帳號密碼是否符合
3. 若符合,server會回傳一組JWT給client
4. client將收到的jwt存在localstorage或cookie中
5. 訪問需要驗證的API會先經過驗證JWT的middleware進行驗證<br>有通過就過、沒通過就檔掉
這麼一來server是不需要儲存每個使用者個登入狀態的,只需要驗證JWT是否有效
### WHAT IS JWT AND HOW IT WORKS
[jwt官網](https://jwt.io/)
JWT是一組加密字串,主要由Header、payload、signature三個部分組成,它的產生過程稱為signing<br>在server驗證完使用者身分後會將使用者id(payload)、加密方法(header)以及一個密鑰(secret)產生signature,而payload + header + signature會形成JWT送到client端,當client端發送需要驗證的request時就會驗證帶進來的JWT,<font color='red'>由於JWT是由payload + header + signature組成,這時候的JWT會被拆解成payload + header + signature的形式,而payload及header則會與server中的secret重新組合成原來的signature,若這兩個signature不相同則代表client端的JWT有被更改或失效了,無法通過驗證</font>


<font color='red'>
### IT'S NOT A JOKE
驗證的功能非常重要,這種東西算是工程師的責任,如果用了套件功能但又因自己對該套件一知半解而導致被駭,這都是寫的人的問題,不應該去怪套件,所以必須放更多的心思在驗證功能上,這樣對大家都是好的</font>
### 登入發送JWT
會使用到這個jwt套件
[jsonwebtoken github](https://github.com/auth0/node-jsonwebtoken)
安裝並引入後就能開始寫了
先到userModel的地方寫驗證密碼的方法
```javascript=
// userModel.js
// instance method, available for all user document
userSchema.methods.correctPassword = async function(candidatePass, userPass) {
return await bcrypt.compare(candidatePass, userPass) // 由於DB內存的密碼是加密過的,所以要用bcrypt.compare來比較輸入的密碼與資料庫內的密碼
}
```
在到驗證的controller寫後續功能
```javascript=
// authController.js
// 定義一個能利用id產生jwt的函數
const signToken = (id) => {
return jwt.sign({id}, process.env.JWT_SECRET, {expiresIn: process.env.JWT_EXPIRES_IN})
}
```
這邊在token的地方會用到jwt.sign(payload, secret, option)來產生JWT,secret的部分我存在環境變數中<br><font color='red'>這邊要提到一下secret的用法: 由於這個套件預設是使用HS256演算法來進行加密,而HS256的secret建議長度要是32以上的字串,所以可以隨便打32個字出來當作自己的secret</font>
```javascript=
// authController.js
// 登入功能
exports.logIn = catchAsync(async(req, res, next) => {
const {email, password} = req.body
// 1. check if email and password exist
if(!email || !password){
return next(new ApiError('please provide email or password',400))
}
// 2. check if user exist && password is correct
const user = await User.findOne({email}).select('+password')
if(!user || !await user.correctPassword(password, user.password)) { // only check password if there's a user
return next(new ApiError('invalid email or password',401))
}
// 3. send a token to client if everything is fine
const token = signToken(user._id)
res.status(200).json({
status: 'success',
token
})
})
```
登入得到的response:
```
{
"status": "success",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwNDgxZjk5NGRhZDQ0NzVjNGUwOGZjZSIsImlhdCI6MTYxNTMzOTcwOCwiZXhwIjoxNjIzMTE1NzA4fQ.F2a2bzGkCAAjt8Ll-qMDZ8h8XcV3vIYAfs03YM0zzwU"
}
```
### 使用JWT驗證valid request
先測試一下server要如何接收client端的JWT
1. 包在request header裡
2. 包在cookie裡
寫驗證middleware
驗證middleware的大致架構如下
```javascript=
// authControll.js
exports.protect = catchAsync(async(req, res, next) => {
// 1. get token and check if it exists
// 2. token verification
// 3. check if user still exist
// 4. check if user have changed password after the token was issued
// 5. if all fine, go next
next()
})
```
1. 取得並檢查Token是否存在:<br>這裡先採用將token放在header內的方法
```javascript=
exports.protect = catchAsync(async(req, res, next) => {
// 1. get token and check if it exists
let token
if(req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1]
}
if(!token) {
next(new ApiError('not logged in',401))
}
})
```
2. 驗證token<br>使用node內建的promisify功能(以此呼叫的函式會以promise形式回傳)呼叫jwt的verify功能,而verify會利用存在伺服器中的secret來解析回來的token,最後在檢查解析出來的id是否有符合使用者id
```javascript=
// 2. token verification
const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET)
console.log(decoded) // 可以看看解析後的樣子
})
```
中場測試
先登入一次把或得的token拿去jwt官網做一點改造後再用該token訪問受驗證middleware保護的route確認是否真的有在驗證
```
{
"status": "error",
"error": {
"name": "JsonWebTokenError",
"message": "invalid signature",
"statusCode": 500,
"status": "error"
},
"message": "invalid signature"
}
```
OK
3. 將步驟2解析出來的id拿去搜尋使用者
```javascript=
// 3. check if user still exist
const currentUser = await User.findById(decoded.id)
if (!currentUser){
return next(new ApiError(`User of this token doesn't exist`, 401))
}
```
4. 檢查密碼有無更改
```javascript=
// 4. check if user have changed password after the token was issued
if (currentUser.changedPasswordAfter(decoded.iat)) { // iat為該JWT創造時間
return next(new ApiError('User password changed, log in again', 401))
}
```
此步驟會在model中新增一個instance method供檢查密碼變更時間
```javascript=
// userModel.js
userSchema.methods.changedPasswordAfter = function(JWTTimestamp) {
// 變更密碼時會在document中新增一個更改的時間,若該資料存在則進行後續判斷
if(this.passwordChangedAt){
const changedTimestamp = parseInt(this.passwordChangedAt.getTime() / 1000,10)
return JWTTimestamp < changedTimestamp // 比較 JWT創立時間及密碼變更時間,若JWTTimestamp時間較小,代表在創造JWT後有改過密碼,回傳true使第4到關卡攔截成功
}
// doesn't change
return false
}
```
## 權限設定
前面做完了驗證,但這樣其實還不夠完善,很多網站或APP都會有設有管理員帳號,該帳號可執行需要高級權限的動作(例如刪除其他用戶帳號)
所以在訪問這些動作的功能中,除了驗證是否已經登入,還要再驗證權限是否符合
在這個部分會示範如何區分用戶權限及相關middleware設定
1. 在userModel加入user role
```javascript=
const userSchema = new mongoose.Schema({
// .............other things.............
role: {
type: String,
enum: ['user', 'guide', 'lead', 'admin'],
default: 'user'
},
// .............other things.............
})
```
2. 高權限帳號與普通帳號個創一個來測試
3. 到authController寫middleware
```javascript=
exports.restriction = (...roles) => {
return (req, res, next) => {
// roles is an array ['admin', 'lead-guide']
if(!roles.includes(req.user.role)){
return next(new ApiError('No permission to do this', 403))
}
next()
}
}
```
4. 到要使用的router中引入middleware
```javascript=
// .............other things.............
const { protect, restriction } = require('../controller/authController')
// .............other things.............
router
.route('/:id')
.get(tourController.getTour)
.patch(tourController.updateTour)
.delete(
protect,
restriction('admin', 'lead-guide'), // 權限middleware加在delete功能之前,
tourController.deleteTour)
```
5. 分別用普通張號與符合限制的帳號來進行刪除測試
## 重置密碼
使用網站很常用到的功能,通常會寄一封email讓你直接reset密碼
流程如下
1. 使用者對忘記密碼的連結發送含有email的request
2. 伺服器收到後對該email寄出含有reset token的信件
3. 使用者打開email中的連結到設定新密碼的地方
4. 新的密碼和reset token一起寄回server
5. server更新使用者密碼
### 會用到的套件
[nodemailer](https://nodemailer.com/about/)
[mailtrap](https://mailtrap.io/)
### 到router中定義要使用的route
```javascript=
// userRouter.js
// .............other things.............
router.post('/forgot', authController.forgotPass)
router.post('/reset', authController.resetPass)
// .............other things.............
```
### controller功能
```javascript=
// authController.js
// .............other things.............
exports.forgotPass = catchAsync(async(req, res, next) => {
// 1. get user email
const user = await User.findOne({email: req.body.email})
if(!user) return next(new ApiError(`user doesn't exist`, 404))
// 2. generate a random reset token
const resetToken = user.createResetToken()
await user.save({ validateBeforeSave: false})
// 3. send it to user
})
// .............other things.............
```
其中的createResetToken()是寫在model中的instance method
```javascript=
userSchema.methods.createResetToken = function() {
const resetToken = crypto.randomBytes(32).toString('hex')
this.passwordResetToken = crypto.createHash('sha256').update(resetToken).digest('hex')
console.log({resetToken}, this.passwordResetToken) // 查看結果
this.passwordResetExpires = Date.now() + 10 * 60 * 1000
return resetToken
}
```
### email功能
註冊好mailtrap後進入信箱開啟SMTP setting選擇nodemail

下面就會有nodemailer設定的程式碼
另外寫一個新的email.js
```javascript=
// email.js
const email = require('nodemailer')
const sendEmail = (options) => {
// 1. create a transporter
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT,
auth: {
user: process.env.EMAIL_NAME,
pass: process.env.EMAIL_PASS
}
})
// 2. define the email options
const mailOptions = {
from: 'ASDFASDF sadfsdf <HELLO@WORLD>', // 寄件者名字
to: options.email, // 收件者email
subject: options.subject, // 信件標題
text: options.message // 信件內容
}
// 3. send email with nodemailer
await transporter.sendMail(mailOptions) // 寄出email
}
```
### email功能整合進controller
```javascript=
// authController.js
const sendEmail = require('../utils/email')
//**other things**
exports.forgotPass = catchAsync(async(req, res, next) => {
// 1. get user email
const user = await User.findOne({email: req.body.email})
if(!user) return next(new ApiError(`user doesn't exist`, 404))
// 2. generate a random reset token
const resetToken = user.createResetToken()
await user.save({ validateBeforeSave: false})
// 3. send it to user
const resetURL = `${req.protocol}://${req.get('host')}/api/v1/users/reset/${resetToken}`
const message = `forgot your password? send your new password to ${resetURL} with a patch request, if you didn't, just ignore this email`
try {
await sendEmail({
email: user.email,
subject: 'PASSWORD RESET',
message,
})
res.status(200).json({
statis: 'success',
message: 'token sent'
})
} catch (error) {
user.passwordResetToken = undefined
user.passwordResetExpires = undefined
await user.save({ validateBeforeSave: false})
return next(new ApiError('OPPS! An error occurred when sending the email', 500))
}
})
```
### 重置密碼功能
已經能夠成功寄出email了,在來就是根據我們上一步建立的函式中產生的reset token建立一個路徑,這個路徑會接收新的密碼,收到後更改對應使用者的密碼
```javascript=
exports.resetPass = catchAsync(async (req, res, next) => {
// 1. get user based on the token
const hashedToken = crypto
.createHash('sha256')
.update(req.params.token)
.digest('hex')
console.log('hashedToken ='+ hashedToken)
const user = await User.findOne({
passwordResetToken: hashedToken,
passwordResetExpires: { $gte: Date.now() }
})
// 2. if the token hasn't expired, and user exist, set new password
if(!user) return next(new ApiError('Token is invalid or has expired',400))
user.password = req.body.password
user.passwordConfirm = req.body.passwordConfirm
user.passwordResetToken = undefined
user.passwordResetExpires = undefined
await user.save() // y dont use update? cuz save() triggers all of the validation process again
// 3. update passwordChangedAt property of current user(in model middleware)
// 4. log user in, send JWT
const token = signToken(user._id)
res.status(200).json({
status: 'success',
token
})
})
```
## 修改密碼
與重置太一樣,修改密碼是在使用者已經登入的情況下修改密碼
1. 定義route
```javascript=
// userRouter.js
router.patch('/updatepass',authController.protect ,authController.updatePass)
```
2. 撰寫功能
```javascript=
exports.updatePass = catchAsync(async(req, res, next) => {
// 1. get user from collection
const user = await User.findById(req.user.id).select('+password')
// 2. check if old password is correct
if(!await user.correctPassword(req.body.oldpass, user.password)) return next(new ApiError('wrong password', 401))
// 3. update password
user.password = req.body.newpass
user.passwordConfirm = req.body.passwordConfirm
await user.save()
// 4. log user in, send JWT
createSendToken(user, 200, res)
})
```
## 修改其他資料
```javascript=
// userController.js
// filter out unwanted fields
const filterObject = (obj, ...allowFields) => {
const newObj = {}
Object.keys(obj).forEach(item => {
if(allowFields.includes(item)){
newObj[item] = obj[item]
}
})
return newObj
}
exports.updateMe = async(req, res, next) => {
// 1. create error if user posts password data
if(req.body.password || req.body.passwordConfirm) return next(new ApiError('this route is not for password update', 400))
// 2. filter unwanted fieldnames which are not allow to be updated
const filteredBody = filterObject(req.body, 'name', 'email') // only allow name and email to be updated
// 3. update user account
const updatedUser = await User.findByIdAndUpdate(req.user.id, filteredBody, {
new: true,
runValidators: true
})
res.status(200).json({
status: 'success',
data: {
user: updatedUser
}
})
}
```
## 刪除用戶
說是刪除其實只是把用戶的active狀態改成false,之後再利用query middleware在find的時候不要將active為false的資料抓出來
```javascript=
// userController.js
exports.deleteMe = catchAsync(async(req,res,next) => {
await User.findByIdAndDelete(req.user.id, {active: false})
res.status(204).json({
status: 'success',
data: null
})
})
```
```javascript=
// userModel.js
userSchema.pre(/^find/, function (next) {
this.find({ active: { $ne: false} })
next()
})
```
## Token in Cookie
將authController中的createSendToken做一些改變,讓token可以放在cookie裡傳送
```javascript=
const createSendToken = (user, statusCode, res) => {
const token = signToken(user._id)
const cookieOptions = {
expires: new Date(
Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000
),
secure: false, //only send in https,
httpOnly: true //
}
if(process.env.NODE_ENV==='production') cookieOptions.secure=true
res.cookie('jwt', token, cookieOptions)
res.status(statusCode).json({
state: 'success',
token,
data: {
user
}
})
}
```
之後到postman做簡單測試
看到下面有cookies代表成功了

## Spamming prevent
為了防止同一個IP快速發送重複request,會用到[express-rate-limit](https://www.npmjs.com/package/express-rate-limit)
這個功能會在全域middleware中實現
```javascript=
// app.js
const rateLimit = require('express-rate-limit')
// ***other things***
const limiter = rateLimit({
// maximum 100 request from same ip in an hour
max: 100,
windowMs: 60*60*1000,
message: 'too many request, rest for an hour'
})
app.use('/api',limiter)
// ***other things***
```
發個request試試,如果在header看到有rate limit的東西代表成功了

## Security http header
[helmet](https://www.npmjs.com/package/helmet)
```javascript=
//app.js
const helmet = require('helmet')
app.use(helmet())
// ***other things***
```
## Data sanitization
Data sanitization主要是為了防止XSS和noSQL query injection
1. noSQL query injection是在request中加入query指令
<br>看到response了嗎?成功登入啦!<br> 這樣也能夠登入就是noSQL query injection的威力
安裝 [express-mongo-sanitize](https://www.npmjs.com/package/express-mongo-sanitize)
```javascript=
// app.js
const mongoSanitize = require('express-mongo-sanitize')
app.use(mongoSanitize())
```
2. XSS
其實mongoose在schema能做的設定已經可以檔掉絕大部分的xss
但還是提一下
安裝[xss-clean](https://www.npmjs.com/package/xss-clean)
```javascript=
// app.js
const xss = require('xss-clean')
app.use(xss())
```
## Prevent parameter pollution
[hpp](https://www.npmjs.com/package/hpp)
```javascript=
//app.js
const hpp = require('hpp')
app.use(hpp({
whitelist: ['duration']
}))
```
## 將controller的方法以工廠函數的方式整合