前言:一個看似簡單的需求
需求很單純:讓 Email 驗證信中的連結顯示前端網址(www.example.com),而不是後台網址(api.example.com)。這樣用戶不會看到內部系統架構。
外包商寫了這段程式碼:
// ❌ 看起來合理,但完全沒效果
plugin.services.user.sendConfirmationEmail = async function(user) {
// 自訂邏輯...
}
部署後,Email 中的連結依然指向後台。為什麼?
問題根源:Factory Function 陷阱
翻開 Strapi 原始碼,發現 plugin.services.user 不是一個物件,而是一個 factory function:
// Strapi 內部實作(簡化版)
plugin.services.user = (context) => {
return {
sendConfirmationEmail: async (user) => { /* 原始邏輯 */ },
// 其他方法...
}
}
這意味著什麼?
每次 Strapi 需要 user service 時,它會呼叫這個 factory function 來取得一個新的 service 實例。你直接覆蓋 sendConfirmationEmail 屬性,等於在一個 function 物件上加屬性——factory 被呼叫時根本不會讀取這個屬性。
不這樣理解會怎樣? 你會像外包商一樣,花兩天 debug 卻找不到原因,因為程式碼完全沒有報錯,只是靜靜地被忽略。
解決方案:Factory Wrapper 模式
正確的做法是包裝原本的 factory function:
// ✅ Factory Wrapper 模式
const originalUserServiceFactory = plugin.services.user;
plugin.services.user = (context) => {
// 先取得原始 service 實例
const originalUserService = originalUserServiceFactory(context);
return {
...originalUserService, // 保留所有原始方法
// 覆寫特定方法
async sendConfirmationEmail(user) {
// 你的自訂邏輯
const confirmationUrl = `${FRONTEND_URL}/verifyEmail`;
// ...
},
};
};
為什麼這樣有效?
- 保存原始 factory:
originalUserServiceFactory是對原始 factory 的引用 - 呼叫原始 factory:取得完整的原始 service 實例
- 展開並覆寫:用 spread operator 保留原始方法,只覆寫需要的部分
關鍵洞見:你不是在覆蓋一個方法,而是在替換整個 factory function,讓它返回你修改過的 service。
用比喻理解 Factory Wrapper
想像一家汽車工廠:
| 做法 | 比喻 | 結果 |
|---|---|---|
| ❌ 直接覆寫 | 在工廠大門貼一張「我們的車會飆車」的紙條 | 工廠內部還是照原本方式生產,紙條沒人看 |
| ✅ Factory Wrapper | 在工廠外面再蓋一層,攔截每台出廠的車,改裝後再交給客戶 | 每台車都被改裝過 |
Factory Wrapper 重點整理
| 概念 | 說明 |
|---|---|
| Factory Function | 每次呼叫都會產生新物件的函式 |
| 為什麼直接覆寫沒用 | 你改的是函式本身,不是它 return 的物件 |
| Factory Wrapper | 用新 factory 包住舊 factory,攔截並修改輸出 |
| 核心步驟 | 保存 → 替換 → 呼叫原始 → 展開 → 覆寫 |
第二個陷阱:Email 模板的 Query String
修好了 factory wrapper,結果收到的 Email 長這樣:
https://example.com/verifyEmail?confirmation=abc123?confirmation=abc123
為什麼有兩個 ?confirmation=?
Strapi 的 email 模板使用變數:
<a href="<%= URL %>?confirmation=<%= CODE %>">驗證</a>
而我的程式碼傳入:
{
URL: `${FRONTEND_URL}/verifyEmail?confirmation=${token}`, // ❌ 已經有 query string
CODE: token,
}
模板把 ?confirmation= 又加了一次。
修正:URL 只傳基底路徑,讓模板負責加 query string:
{
URL: `${FRONTEND_URL}/verifyEmail`, // ✅ 只有路徑
CODE: token,
}
第三個陷阱:錯誤的 Nginx 設定檔
要讓前端 URL 可以驗證,需要 Nginx 做重導向。我修改了 docker/nginx/prod.conf,但部署後沒生效。
檢查 Dockerfile:
# 實際使用的是這個檔案!
COPY ./docker/nginx/bot-routing.conf /etc/nginx/http.d/default.conf
我改錯檔案了。 prod.conf 根本沒被使用。
教訓:修改設定前,先確認 Dockerfile 實際使用哪個檔案。
補充:什麼是反向代理?
在這個解決方案中,Nginx 扮演「反向代理」的角色。讓我用比喻解釋:
正向代理(Forward Proxy):你的代理人
想像你要買一棟豪宅,但不想讓賣家知道是你在買(怕抬價)。你請了一個代理人(律師)出面談判,賣家只知道「有個律師要買房」,不知道背後是誰。
正向代理就是這樣:
- 你(Client) 知道自己要連哪個網站
- 代理人(Proxy) 幫你去連
- 網站(Server) 只看到代理人,不知道你是誰
常見用途:翻牆、隱藏身份、公司網路管控。
反向代理(Reverse Proxy):總機小姐
想像你打電話到一家大公司,接電話的是總機小姐。你說「我要找技術支援」,總機幫你轉接到正確的部門。你不知道(也不需要知道)技術支援部門的直撥號碼。
反向代理就是這樣:
- 你(Client) 只知道一個入口(總機)
- 總機(Nginx) 根據你的需求,轉接到對應的後端服務
- 後端服務(Server) 對外隱藏,只有總機知道怎麼找到它們
在我們的案例中:
用戶點擊: www.example.com/verifyEmail
↓
[Nginx 反向代理]
「喔,你要驗證 Email?我幫你轉給後台 API」
↓
api.example.com/api/auth/email-confirmation
為什麼要這樣做?
- 安全性:用戶不知道後台網址,減少攻擊面
- 靈活性:後台可以隨時搬遷,只要改 Nginx 設定
- 形象:用戶看到的都是漂亮的前端網址
反向代理解決什麼問題?
回到我們的需求:Email 驗證信的連結。
原本的流程(沒有反向代理):
Email 連結: api.example.com/api/auth/email-confirmation?confirmation=TOKEN
↓
用戶心裡: 「api 是什麼?這網址看起來怪怪的...」
加入 Nginx 後:
Email 連結: www.example.com/verifyEmail?confirmation=TOKEN
↓
[Nginx 反向代理]
↓
實際處理: api.example.com/api/auth/email-confirmation?confirmation=TOKEN
↓
用戶心裡: 「正常的網址,沒什麼特別的」
| 問題 | 沒有反向代理 | 有反向代理 |
|---|---|---|
| 用戶體驗 | 看到奇怪的 api. 網址 | 只看到熟悉的前端網址 |
| 安全性 | 後台網址暴露 | 後台網址隱藏 |
| 靈活性 | 換後台要改 Email 模板 | 換後台只要改 Nginx 設定 |
完整架構
最終解決方案需要兩個元件配合:
Strapi(產生前端 URL):
// strapi-server.ts
const confirmationUrl = `${FRONTEND_URL}/verifyEmail`;
// Email 模板會加上 ?confirmation=TOKEN
Nginx(反向代理到後端 API):
# bot-routing.conf
map $host $backend_api_url {
"~*stg-www.example.com" "https://stg-api.example.com";
"~*www.example.com" "https://api.example.com";
}
location = /verifyEmail {
if ($arg_confirmation) {
return 302 $backend_api_url/api/auth/email-confirmation?confirmation=$arg_confirmation;
}
}
流程:
- 用戶點擊
www.example.com/verifyEmail?confirmation=TOKEN - Nginx(總機)302 重導向到
api.example.com/api/auth/email-confirmation - Strapi 驗證 token 並更新用戶狀態
- 用戶回到 App,按「下一步」成功
關鍵學習
| 陷阱 | 症狀 | 解法 |
|---|---|---|
| Factory Function | Override 沒效果,無報錯 | Factory Wrapper 模式 |
| Email 模板變數 | Query string 重複 | URL 只傳路徑 |
| 錯誤的設定檔 | 設定沒生效 | 先檢查 Dockerfile |
最重要的洞見:當你的 override「沒有報錯但也沒效果」時,很可能是你在覆寫一個 factory function,而不是一個普通物件。去看原始碼,確認你要覆寫的東西是什麼型別。
這個 debug 過程花了一整天,希望這篇文章能幫你省下這一天。
