前面已經完成 Google Cloud 專案設定、啟用 Google Calendar API、建立 OAuth 2.0 憑證,以及產生 Google 授權網址。
接下來要讓 Node.js 真正取得 Google 帳號的授權,並透過 Google Calendar API 讀取行事曆及建立事件。
這六天主要完成:
前一天已經產生 Google OAuth 2.0 授權網址,但使用者同意授權後,程式還需要接收 Google 回傳的 Authorization Code。
因此使用 Node.js 的 http 模組建立本機 HTTP Server,接收 Google 的授權回呼。
const server = http.createServer(async (req, res) => {
const incoming = new URL(
req.url,
"http://127.0.0.1"
);
if (incoming.pathname !== "/oauth2callback") {
res.writeHead(404);
res.end("Not found");
return;
}
});
這裡使用 http.createServer() 建立伺服器,並透過 incoming.pathname 確認請求是否來自指定的 Callback 路徑。
接著加入 State 驗證:
const state = crypto
.randomBytes(32)
.toString("hex");
State 會在產生授權網址時一起傳送給 Google,授權完成後再檢查回傳值是否一致。
if (incoming.searchParams.get("state") !== state) {
res.writeHead(400);
res.end("授權狀態不符");
return;
}
為什麼需要 State?
因為 OAuth 2.0 會透過瀏覽器重新導向傳遞授權結果,程式必須確認收到的回應對應到自己原本發出的授權請求,降低 CSRF 攻擊風險。
驗證成功後,就可以接收 Authorization Code:
const code = incoming.searchParams.get("code");
if (!code) {
res.writeHead(400);
res.end("缺少授權碼");
return;
}
console.log("Day 14:已收到 Authorization Code");

到這裡,程式已經能接收 Google 授權回呼,取得後續交換 Token 所需要的 Authorization Code。
取得 Authorization Code 後,還不能直接呼叫 Google Calendar API。
因為 Authorization Code 的用途是交換 Token,真正呼叫 API 時主要使用 Access Token。
這次使用 PKCE(Proof Key for Code Exchange)加強授權流程的安全性。
首先產生 Code Verifier 與 Code Challenge:
const {
codeVerifier,
codeChallenge
} = await client.generateCodeVerifierAsync();
Code Challenge 會隨授權請求送出,而 Code Verifier 會保留至交換 Token 時使用。
這樣 Google 就可以確認交換授權碼的程式持有原本產生的 Code Verifier,降低授權碼遭攔截後被冒用的風險。
接著使用 getToken() 交換 Token:
const { tokens } = await client.getToken({
code,
codeVerifier,
redirect_uri: REDIRECT_URI
});

取得 Token 後,將授權資訊設定到 OAuth2Client:
client.setCredentials(tokens);
這裡有兩種重要的 Token:
實際執行後,終端機顯示:
Day 15:已取得 Access Token;Refresh Token:已取得
代表這次授權成功取得 Access Token 與 Refresh Token。
前一天已經成功取得 Token,但如果只將 Token 保存在程式記憶體中,Node.js 結束後就無法直接在下次執行時重複使用。
因此今天使用 Node.js 的 fs 模組,把 Token 儲存成 token.json。
const fs = require("fs");
const path = require("path");
const TOKEN_PATH = path.join(
__dirname,
"../token.json"
);
接著將 Token 轉換成 JSON 格式並儲存:
fs.writeFileSync(
TOKEN_PATH,
JSON.stringify(tokens, null, 2),
{ mode: 0o600 }
);
當程式重新啟動時,先檢查檔案是否存在,再讀取 Token:
if (fs.existsSync(TOKEN_PATH)) {
const savedTokens = JSON.parse(
fs.readFileSync(TOKEN_PATH, "utf8")
);
client.setCredentials(savedTokens);
}
為什麼需要儲存 Token?
因為使用者不應該每次啟動 Discord Bot 都重新開啟 Google 授權頁面。
有了 token.json,程式就能重複使用既有的授權資訊。
當 Access Token 過期時,如果存在有效的 Refresh Token,Google Auth Library 也能在需要時嘗試更新 Access Token。
另外,由於 token.json 包含敏感授權資料,因此需要將相關檔案加入 .gitignore:
token.json
credentials.json
.env
實際執行結果:
Day 16:已儲存 token.json(不顯示 Token)
Day 16:成功讀取 token.json 並取得有效 Access Token
代表 Token 儲存與讀取流程已經成功。
完成 OAuth 2.0 授權後,接下來需要建立 Google Calendar API Client,才能使用程式操作 Google Calendar。
這裡需要區分兩個物件:
OAuth2Client 負責管理授權資訊。
Calendar API Client 負責提供讀取、新增、修改及刪除行事曆事件的方法。
使用 googleapis 建立 Calendar API Client:
const { google } = require("googleapis");
const calendar = google.calendar({
version: "v3",
auth: client
});
其中:
version: "v3" 指定使用 Google Calendar API v3auth: client 使用前面已完成授權的 OAuth2Client為什麼需要這個步驟?
因為 OAuth2Client 主要負責身分授權,並不直接提供所有 Google Calendar 的操作介面。
建立 Calendar API Client 後,才能透過 calendar.events.insert()、calendar.events.list() 等方法操作行事曆。
執行結果:
Day 17:已建立授權的 Calendar API Client
代表 Calendar API Client 已經成功初始化。
建立 Calendar API Client 後,先測試能否成功讀取 Google Calendar 資料。
這裡使用 calendarList.list() 取得目前帳號的行事曆清單:
const response = await calendar.calendarList.list();
const calendars = response.data.items || [];
response.data.items 包含 API 回傳的行事曆項目。
接著使用 map() 整理資料,並透過 console.table() 顯示:
console.table(
calendars.map((item) => ({
名稱: item.summary,
主行事曆: item.primary === true,
權限: item.accessRole
}))
);
這裡主要讀取三個欄位:
summary:行事曆名稱primary:是否為主要行事曆accessRole:目前使用者對該行事曆的存取權限為什麼要先讀取清單?
因為同一個 Google 帳號可能有多個行事曆,而且不同的行事曆不一定都有修改權限。
例如:
owner 代表擁有者。
writer 代表可以讀取及修改事件。
reader 代表只有讀取權限。
因此,在新增事件之前,需要確認目標行事曆是否具有足夠的寫入權限。
這次成功取得五個行事曆項目,包括主要行事曆、課程行事曆及其他訂閱行事曆。
其中主要行事曆的 primary 為 true,accessRole 為 owner。
代表程式已經可以成功讀取 Google Calendar 資料,也能辨識不同日曆的存取角色。

前一天已經成功取得 Google Calendar 行事曆清單,接下來要測試新增事件,確認 Node.js 不只能讀取資料,也能寫入 Google Calendar。
這次使用 insertTestEvent() 函式建立測試事件,並指定主要行事曆 primary。
程式會自動計算開始與結束時間,將測試行程安排在執行時間約一小時後,行程長度設定為 30 分鐘。
接著透過 events.insert() 新增事件:
event = (await calendar.events.insert({
calendarId: "primary",
sendUpdates: "none",
requestBody: {
id,
summary: "Day 19|Google Calendar API 測試",
description: "Discord Calendar Bot 課程:第一筆 API 測試行程。",
start: {
dateTime: start.toISOString(),
timeZone: "Asia/Taipei"
},
end: {
dateTime: end.toISOString(),
timeZone: "Asia/Taipei"
},
reminders: {
useDefault: false
}
}
})).data;
為了避免重複執行時建立多筆相同的測試行程,這次也加入固定事件 ID,並先使用 events.get() 檢查事件是否已存在。
如果事件不存在才新增,若已存在則直接讀取既有事件。
新增完成後,再使用 events.get() 重新查詢:
event = (await calendar.events.get({
calendarId: "primary",
eventId: event.id
})).data;
這樣可以確認事件確實已經儲存在 Google Calendar,而不只是程式送出新增請求。
圖片:Day 19 events.insert() 新增行程與讀回驗證實作
在終端機執行:
node src/googleCalendar.js test-event
執行後,程式成功讀取 Token、建立 Calendar API Client,並建立測試事件。
終端機顯示:
Day 19:測試行程已確認
開始時間:2026-10-15T11:33:00+08:00
Google Calendar 連結:已產生

代表測試事件建立及查詢流程已完成。
圖片:Day 19 成功建立測試行程並讀回確認
完成 Google Calendar API 的新增測試後,也進一步測試 Discord Bot 的 /add 指令。
在 Discord 輸入行程名稱、日期及時間後,Bot 成功回覆:
已新增至 Google 行事曆
行程名稱:Discord新增測試
日期:2026-10-15
時間:14:00(台灣時間,1小時)
並附上 Google Calendar 事件連結。

最後開啟 Google Calendar,確認兩筆測試行程都已出現在日曆中。
其中一筆是透過 Node.js 直接呼叫 Calendar API 建立的測試事件,另一筆則是透過 Discord /add 指令新增的行程。
這證明目前已經可以從 Discord 輸入行程資訊,經由 Bot 呼叫 Google Calendar API,將事件建立到 Google Calendar。

Day 14~19 已完成 Google OAuth 2.0 授權流程、Token 儲存與讀取、Calendar API Client 建立、行事曆清單查詢及事件新增。
此外,也初步完成 Discord /add 指令與 Google Calendar API 的整合。
接下來將繼續完善 Discord Bot 的行程管理功能,包括新增結果處理、查詢行程及刪除行程。
這六天主要完成 Google OAuth 2.0 授權與 Google Calendar API 整合的基礎流程。
整體資料流程如下:
events.insert() 嘗試新增行事曆事件目前已成功完成授權、Token 管理、Calendar API Client 初始化與行事曆讀取。
新增事件功能已完成基本程式實作,但仍需要排除 API 錯誤。
後續 Day 20~25 將以 Discord Bot 為主要開發方向,逐步整合 /add、/list 與 /delete 指令,讓使用者可以直接透過 Discord 管理 Google Calendar 行程。