邱玉躍
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 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的方法以工廠函數的方式整合

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully