前言:一個看似簡單的需求

需求很單純:讓 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`;
      // ...
    },
  };
};

為什麼這樣有效?

  1. 保存原始 factoryoriginalUserServiceFactory 是對原始 factory 的引用
  2. 呼叫原始 factory:取得完整的原始 service 實例
  3. 展開並覆寫:用 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;
    }
}

流程

  1. 用戶點擊 www.example.com/verifyEmail?confirmation=TOKEN
  2. Nginx(總機)302 重導向到 api.example.com/api/auth/email-confirmation
  3. Strapi 驗證 token 並更新用戶狀態
  4. 用戶回到 App,按「下一步」成功

關鍵學習

陷阱症狀解法
Factory FunctionOverride 沒效果,無報錯Factory Wrapper 模式
Email 模板變數Query string 重複URL 只傳路徑
錯誤的設定檔設定沒生效先檢查 Dockerfile

最重要的洞見:當你的 override「沒有報錯但也沒效果」時,很可能是你在覆寫一個 factory function,而不是一個普通物件。去看原始碼,確認你要覆寫的東西是什麼型別。

這個 debug 過程花了一整天,希望這篇文章能幫你省下這一天。