# 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的方法以工廠函數的方式整合