---
# System prepended metadata

title: Express
tags: [Nodejs]

---

# Express
###### tags: `Nodejs`

Express是基於nodejs寫的一個框架，它將很多功能都包裝得更好用、大幅增加了開發速度
![](https://i.imgur.com/bPjghwX.png)


## API
API可以被當成是一個程式之間溝通的媒介，例如要使用node下的fs或http等功能就要透過node API來進行操作、操作DOM元素也是要透過javascript的DOM API來進行操作
![](https://i.imgur.com/8GA0jUe.png)

## RESTfulAPI

### 設計標準
![](https://i.imgur.com/rDPQFQo.png)
1. 將API以資源方式區分
![](https://i.imgur.com/ydLNRpr.png)
例如跟tours相關的資料就放在tours裡
2. 以資源命名的url
![](https://i.imgur.com/IX5MVA3.png)
3. 以http method來區分不同用法
例如我addTours的功能和deleteTours的功能都在/tours的路徑
用post方法訪問/tours時就會觸發addTours
用delete方法訪問/tours時就會觸發deleteTours
4. 以JSend形式傳送資料
一般的JSON長這樣
![](https://i.imgur.com/7LzcXZp.png)
建議傳送出去前先處理成這樣再傳(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
  })
})
```
![](https://i.imgur.com/RfIeGa1.png)
5. Stateless
![](https://i.imgur.com/XK4BJSb.png)
狀態部分一律在client端處理(目前頁數、登入狀態)，後端只負責送資料，不需要去記錄狀態
可以將不必要負荷分給client端
以翻頁為例
![](https://i.imgur.com/bA2uBmm.png)

### 路由配置
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)
![](https://i.imgur.com/bDYRtkX.png)
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

![](https://i.imgur.com/3R3LUPG.png)
#### Controller(Application logic)
![](https://i.imgur.com/Xvhvjkt.png)

#### Model(Business logic)
![](https://i.imgur.com/6nihMmx.png)

#### 設計要點: Fat model thin controller
盡量將複雜的邏輯放在Model，讓controller可以擔任好橋樑的角色

## 錯誤處理
程式的錯誤通常分為
1. 作業錯誤Operational error: <br>作業程序上的錯誤，通常指route錯誤、輸入資料錯誤、伺服器連接失敗、連接超時等可以被預料到的錯誤<br>![](https://i.imgur.com/qcWYaYr.png)

2. 程式錯誤Programming error: <br>程式碼上的錯誤，這類比較偏向程式碼沒寫好導致的錯誤，例如常見的read property of undefined、錯誤資料類型、傳錯參數等程式碼上的錯誤<br>![](https://i.imgur.com/ljpS3W0.png)

錯誤處理要處理的錯誤都是Operational error類型的，因為這方面的錯誤通常可以因client端達成正確行為而被改善，在express中可以透過一個middleware來處理所有這方面的錯誤並送response回去通知使用者出錯原因，透過獨立的middleware可以讓錯誤處理獨立出來不影響到其他程式碼
![](https://i.imgur.com/0LeIem6.png)

### 建立錯誤處理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>

![](https://i.imgur.com/NZoDVr2.png)
![](https://i.imgur.com/fdtIlMh.png)
<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內的方法![](https://i.imgur.com/FZI9ubP.png)

```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
![](https://i.imgur.com/vrcwG3Y.png)
下面就會有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代表成功了
![](https://i.imgur.com/GigvPp0.png)

## 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的東西代表成功了
![](https://i.imgur.com/sqDrJ8q.png)

## 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指令
![](https://i.imgur.com/XGN1pty.png)<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的方法以工廠函數的方式整合
