Github repository
(git master commit : 9bc555a335e9c844e014a656cc3d519b7bccd6bc)
讀 Pocket API 認證
Pocket API Authentication
等我有空再把內容寫出來。
整理 Pocket API 認證部分
接下來要實作的是使用者用 Pocket 帳號登入的部分,列一下他的步驟。
所有的 AJAX 請求都要是POST方法,並且附帶以下 header
Content-Type : application/json; charset=UTF-8 X-Accept : application/json
取得request token (決定由後端處理)
POST to https://getpocket.com/v3/oauth/request body = { consumer_key : **************, redirect_uri : 我的應用程式網址, state : 可選,metadata }
回傳的資料會是以下格式
{ code : 我的request_token }
將使用者導向到以下認證頁面,該頁面建議不要新開視窗。 (決定由前端處理)
https://getpocket.com/auth/authorize?request_token=[request_token]&redirect_uri=[redirect_uri]
使用者按下同意應用程式存取指定權限後,會自動導向到redirect_uri。使用者進入 redirect_uri 後,使用 POST 請求,取得access_token。(決定由後端處理)
POST to https://getpocket.com/v3/oauth/authorize body = { consumer_key : **************, code : **request_token** }
回傳的資料會是以下格式
{ username : 使用者名稱, access_token : **************** }
這麼一來就可以存取使用者的 Pocket 清單做各種事。
問題點 A
如何確認使用者已經登入 Pocket 並同意存取?
使用者不一定會乖乖的從我開給他登入的頁面進入,常常是直接開 APP 的網址,我要怎麼確認他的狀態?
結論:在使用者開 APP 網址時,進行一次步驟3,看能不能取得access_token。否則導向回到登入頁。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會按照這樣的流程完成登入程序:
- 使用者進入 login.html
- 使用者點擊 login 按鈕
- 前端傳送 AJAX 給後端,後端向 Pocket API 取得 request_token。
- 後端傳送合併好的認證 URL 給前端。
- 前端頁面導向到認證 URL,等待使用者授權。
- 使用者授權完成,Pocket API 將使用者導向到 redirect_url,也就是 app page。
- 後端 /app 路由接到請求,向 Pocket API 取得 access_token。
- 傳送 app.html,使用者進入app。
以下是路由的流程,不是一開始就規劃好,而是在 router 完成後寫上:
由於還沒有處理好資料庫的部分,所以 request_token 跟 access_token 都先存在 session。
在重開 server 時,session就會刪除。
確認 or 反省
為什麼進入 /app 要先 check requset_token ?
這部分實作上要滿足兩個條件:- 要有 session
- session 中要有 request_token
主要原因是,要取得 access_token ,必須要有 consumer_key 跟 request_token,所以在請求 access_token 之前,先確認 request_token 是否存在。
如果存在,表示已經走過先前的程序,所以繼續往下請求。
反之,回到最初的程序,login,去請求 request_token。
承1,這部分沒考慮到:如果使用者按下 login 按鈕後,直接前往 /app 路由的情形。
(登入程序進行到 5,使用者沒授權直接連 /app)在這情況下,請求 access_token 時,Pocket API 會回傳 403。
所以可能要在 get access_token 後,再增加是否重新導向回 login page 的判定吧。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)
})
}
途中遇到的幾個問題是:
- 對 Node.js 的路徑處理不是很熟,稍微找一下才找到
process.pwd()
取得根目錄。此外也有一些地方也是相對路徑指定不熟…我都反正改一改試一試就出來的心態。 - 簡單掃了一下,fetch 的 option 似乎可以拉出來,或是想個辦法全部合併,因為除了 body 之外通通是一樣的。讓請求的流程被看的更清楚。
收尾 & 全部問題點 & 在意點清單
結果還沒進行到 跨平台測試的部分,這可能是個大坑,留待下次吧。
清單:
- 修改 fetch 的 option
- 整理相對路徑的寫法
- 思考 is Ajax request? 在 mount 路由時的處理方式
- 取得 access_token 的步驟後,新增若 403 就重新導向 /login
- /login 跟 /pocket/login 兩個看起來太像,還是擇一改吧?
- 避免使用 error.pug
- Pocket API 在 行動版、iOS、Mac上的實作狀況
- 是否有比 MVC 架構更好的選擇?因為這專案很小….(大概吧)