0%

pocket-random 開發紀錄 - 2. 實作 Pocket API 認證

Github repository
(git master commit : 9bc555a335e9c844e014a656cc3d519b7bccd6bc)

讀 Pocket API 認證

Pocket API Authentication
等我有空再把內容寫出來。

整理 Pocket API 認證部分

接下來要實作的是使用者用 Pocket 帳號登入的部分,列一下他的步驟。

  1. 所有的 AJAX 請求都要是POST方法,並且附帶以下 header

    Content-Type : application/json; charset=UTF-8
    X-Accept : application/json
  2. 取得request token (決定由後端處理)

    POST to
    https://getpocket.com/v3/oauth/request
    
    body = {
      consumer_key : **************,
      redirect_uri : 我的應用程式網址,
      state : 可選,metadata
    }

    回傳的資料會是以下格式

    {
      code : 我的request_token
    }
  3. 將使用者導向到以下認證頁面,該頁面建議不要新開視窗。 (決定由前端處理)
    https://getpocket.com/auth/authorize?request_token=[request_token]&redirect_uri=[redirect_uri]
    使用者按下同意應用程式存取指定權限後,會自動導向到redirect_uri。

  4. 使用者進入 redirect_uri 後,使用 POST 請求,取得access_token。(決定由後端處理)

    POST to
    https://getpocket.com/v3/oauth/authorize
    
    body = {
      consumer_key : **************,
      code : **request_token**
    }

    回傳的資料會是以下格式

    {
      username : 使用者名稱,
      access_token : ****************
    }

    這麼一來就可以存取使用者的 Pocket 清單做各種事。

問題點 A

  1. 如何確認使用者已經登入 Pocket 並同意存取?
    使用者不一定會乖乖的從我開給他登入的頁面進入,常常是直接開 APP 的網址,我要怎麼確認他的狀態?
    結論:在使用者開 APP 網址時,進行一次步驟3,看能不能取得access_token。否則導向回到登入頁。

  2. Pocket API 文件有提到許多在iOS、Mac實作上的例外狀況,而且也建議,因為 Pocket 有手機 APP 版本,所以在行動版要導向到 手機APP 驗證。
    結論:因為完全不知道實際運行上會怎麼樣,所以先做到桌面網頁版認證成功,丟到heroku上,在測試手機版。

因為問題2的部分,決定先實作出基本前端頁面,跟後端有關認證的部分。

建立Node.js + express 專案

我使用express-generator直接生成一個express專案架構,下圖是我的目錄結構:

  • model : 放 mongoose model
  • views : 放 html 檔案。雖然 view 支援 template 語言,但這次的頁面預計只有兩頁,所以用HTML(好懷念呀~)。但其實考慮到頁面可能會擴充,其實用 template 寫應該比較好。
  • controller : 放各路由的 middleware function,根據路由切檔案。包括一個工具middleware customUtil.js。
  • routes:放路由。
  • auth.js: 裡面有放consumer_key這個敏感資訊。因為會把檔案丟到 github repo 上,所以把這個資訊另外放一個檔案,要用再require進來。然後在 .gitignore 加入他。
  • 兩個 .pem :放 SSL 憑證,是用 mkcert 生出來的。否則不能用 https ,也就是不能用 Chrome 測試。

將 request 換成 fetch

開始著手寫連 API 的函式,上一個專案是用 request 做 HTTP 連線,本來打算沿用,但在查 POST 方法怎麼寫時,發現 request 在今年2月已經 deprecate 了。原因我沒查清楚。
後來看到有人提到 node-fetch 這個套件。
node-fetch的用法跟瀏覽器上的fetch幾乎一模一樣,很好上手,還可以支援 binary 的樣子。
基於這個原因,選擇使用 node-fetch 實作連線 API 的部分。

必須要有 error.pug

express 在發生錯誤時,會選擇 views 目錄中的 error.pug 渲染出錯誤頁面回傳。如果沒有error.pug 就會發生錯誤。
所以雖然這次只有 HTML 檔,但我還是在 views 中放了一個error.pug。
這部分還不知道要怎麼調掉。

將 routes & controller 分成 page 跟 pocket

這次的專案會有前端透過 AJAX 跟後端溝通的部分。 - 當前端按下 login 時,傳 AJAX 到後端,後端跟 API 取得資訊後,再回傳前端。 - 前端可能對文章增加 Pocket 的 tag ,這時要再 AJAX 給後端,後端跟 API 溝通,再回傳前端

種種。
這個專案的路由分成兩種:AJAX、瀏覽器頁面請求。
所以我決定將分別將路由切成page、pocket兩個檔案管理。
(controller 則是對應 router 的結構)
雖然不知道這樣切的理由是否太薄弱。

路由

設想中,APP會按照這樣的流程完成登入程序:

  1. 使用者進入 login.html
  2. 使用者點擊 login 按鈕
  3. 前端傳送 AJAX 給後端,後端向 Pocket API 取得 request_token。
  4. 後端傳送合併好的認證 URL 給前端。
  5. 前端頁面導向到認證 URL,等待使用者授權。
  6. 使用者授權完成,Pocket API 將使用者導向到 redirect_url,也就是 app page。
  7. 後端 /app 路由接到請求,向 Pocket API 取得 access_token。
  8. 傳送 app.html,使用者進入app。

以下是路由的流程,不是一開始就規劃好,而是在 router 完成後寫上:

由於還沒有處理好資料庫的部分,所以 request_token 跟 access_token 都先存在 session。
在重開 server 時,session就會刪除。

確認 or 反省

  1. 為什麼進入 /app 要先 check requset_token ?
    這部分實作上要滿足兩個條件:

    1. 要有 session
    2. session 中要有 request_token
      主要原因是,要取得 access_token ,必須要有 consumer_key 跟 request_token,所以在請求 access_token 之前,先確認 request_token 是否存在。
      如果存在,表示已經走過先前的程序,所以繼續往下請求。
      反之,回到最初的程序,login,去請求 request_token。
  2. 承1,這部分沒考慮到:如果使用者按下 login 按鈕後,直接前往 /app 路由的情形。
    (登入程序進行到 5,使用者沒授權直接連 /app)

    在這情況下,請求 access_token 時,Pocket API 會回傳 403。
    所以可能要在 get access_token 後,再增加是否重新導向回 login page 的判定吧。

  3. is Ajax request? 的判定 以及 重新導向
    在 pocket router 的兩個路由都有這個 middleware。
    這還滿明顯,我是說畫到圖上的時候……………orz

    首先,應該把這個 middleware 寫成共通的處理,比如寫在 router.use 之類的。
    可能我在寫的時候覺得不同的路由會需要回傳到不同的訊息吧?
    這也可以加入考量。

    實際上直接回傳一個 json 或是 string 都比重新導向好,不然路由還要多跑一次/login。

controller

直接上 code

// page.js
const fetch = require('node-fetch');
const auth = require("../auth");
const utils = require("./customUtil");
const path = require("path")

module.exports.getLogin = getLogin;
module.exports.redirectIfNoReqToken = redirectIfNoReqToken;
module.exports.getAccessTokenIfhavent = getAccessTokenIfhavent;
module.exports.toApp = toApp;


function getLogin(req,res,next) {
    res.sendFile( process.cwd() + "/views/login.html")
}

function redirectIfNoReqToken(req,res,next){
    if (req.session && req.session.request_token){
        next()
    }else{
        res.redirect("/login")
    }
}

function getAccessTokenIfhavent(req,res,next){
    if (!req.session.access_token){
        const body = {
            consumer_key : auth.consumer_key,
            code : req.session.request_token
        }
        fetch(utils.pocket_url.authorize,{
            method : "post",
            body : JSON.stringify(body),
            headers : utils.header_base
        })
        .then(response => response.json())
        .then(data => {
            req.session.access_token = data.access_token; 
            req.session.username = data.username;
            next()
        })
    }else{
        next()
    }
}

function toApp(req,res,next){
    res.sendFile( process.cwd() + "/views/app.html")
}



// pocket.js
const fetch = require('node-fetch');
const auth = require("../auth");
const utils = require("./customUtil");

module.exports.redirectIfNotAjax = redirectIfNotAjax;
module.exports.getRequestToken = getRequestToken;
module.exports.sendAuthUrl = sendAuthUrl;
module.exports.rejectIfNotAjax = rejectIfNotAjax;
module.exports.retrieveData = retrieveData;

function redirectIfNotAjax(req,res,next){
    if (req.isAjax){
        next()
    }else{
        res.redirect("/login")
    }
}


function getRequestToken(req,res,next){
    fetch(utils.pocket_url.request,{
        method : "post",
        body : JSON.stringify(auth),
        headers : utils.header_base
    })
    .then(response => response.json())
    .then( response =>{
        req.session.request_token = response.code;
        next()
    })
}
function sendAuthUrl(req,res,next){
    const redirect_url = `${utils.pocket_url.user_auth}?request_token=${req.session.request_token}&redirect_uri=${auth.redirect_uri}`
    const sendBack ={
        redirect_url
    }
    res.json(sendBack)
}

function rejectIfNotAjax(req,res,next){
    if (req.isAjax){
        next()
    }else{
        res.json({msg: "failed"})
    }
}

function retrieveData(req,res,next){
    const body = {
        consumer_key : auth.consumer_key,
        access_token : req.session.access_token
    }
    fetch(utils.pocket_url.retrieve,{
        method : "post",
        body : JSON.stringify(body),
        headers : utils.header_base
    }).then(response => response.json())
      .then(response => {
          res.json(response)
      })
}

途中遇到的幾個問題是:

  1. 對 Node.js 的路徑處理不是很熟,稍微找一下才找到 process.pwd() 取得根目錄。此外也有一些地方也是相對路徑指定不熟…我都反正改一改試一試就出來的心態。
  2. 簡單掃了一下,fetch 的 option 似乎可以拉出來,或是想個辦法全部合併,因為除了 body 之外通通是一樣的。讓請求的流程被看的更清楚。

收尾 & 全部問題點 & 在意點清單

結果還沒進行到 跨平台測試的部分,這可能是個大坑,留待下次吧。

清單:

  1. 修改 fetch 的 option
  2. 整理相對路徑的寫法
  3. 思考 is Ajax request? 在 mount 路由時的處理方式
  4. 取得 access_token 的步驟後,新增若 403 就重新導向 /login
  5. /login 跟 /pocket/login 兩個看起來太像,還是擇一改吧?
  6. 避免使用 error.pug
  7. Pocket API 在 行動版、iOS、Mac上的實作狀況
  8. 是否有比 MVC 架構更好的選擇?因為這專案很小….(大概吧)