新增登入後才能使用的業務頁面
從多語系文案和頁面路由開始,建立能正確處理登入、電子郵件驗證和 2FA 的工作區。
這一篇要建立 /workspace 工作區。訪客必須完成專案要求的身分驗證步驟才能開啟這個頁面,尚未登入、尚未驗證電子郵件或尚未完成 2FA 時,會進入對應的處理頁面。
請先確認本機專案能透過 npm run dev 啟動,而且可以註冊、登入。本篇先建立頁面入口,下一篇再為業務功能新增資料 API。
一、先準備頁面文案
在 locales/en.json、locales/zh-Hans.json 和 locales/zh-Hant.json 中,分別將 workspace 合併到現有的 pages 物件。不要用下方片段覆寫整份語言檔案。
英文:
"workspace": {
"title": "My workspace",
"description": "Manage your saved links here."
}簡體中文:
"workspace": {
"title": "我的工作台",
"description": "在这里管理你收藏的链接。"
}繁體中文:
"workspace": {
"title": "我的工作區",
"description": "在這裡管理你收藏的連結。"
}資源鍵的型別是從英文檔案推導而來,先新增資源再撰寫頁面,就能讓 TypeScript 檢查鍵名。如果專案已啟用其他語言,也要補齊相同的鍵。
二、新增頁面檔案
新增 src/pages/workspace.tsx:
import { serverLocalized } from '@/components/server/Localized';
import { authenticatedGuard } from '@/core/services/auth/guards/authenticated';
import { createLayoutRenderer } from '@/layout';
import { createLayoutHandlers } from '@/libs/renderer/render';
import { getFullPath } from '@/libs/utils';
import type { Ctx } from '@/types';
function WorkspacePage({ c }: { c: Ctx }) {
return (
<main className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold">
{serverLocalized({ c, id: 'pages.workspace.title' })}
</h1>
<p className="mt-4 text-muted-foreground">
{serverLocalized({ c, id: 'pages.workspace.description' })}
</p>
</main>
);
}
const workspacePageHandlers = createLayoutHandlers(
createLayoutRenderer('page'),
async (c: Ctx) => {
const guard = authenticatedGuard(c);
if (!guard.success) {
const redirectUrl = guard.details?.redirectUrl;
if (typeof redirectUrl !== 'string') {
throw new Error('Authenticated guard returned no redirect URL');
}
return c.redirect(getFullPath(redirectUrl, c.var.getLocale()));
}
return <WorkspacePage c={c} />;
},
{ pageRootClassName: 'flex flex-col' },
);
export default workspacePageHandlers;authenticatedGuard 會讀取目前請求執行脈絡中的身分驗證狀態,並依設定檢查登入、電子郵件驗證和 2FA。使用它回傳的 redirectUrl,才能將使用者導向目前需要完成的步驟。
頁面上的條件式呈現無法取代這項檢查。只把內容隱藏起來,或在用戶端載入後才重新導向,都無法保護伺服器回傳的資料。
三、註冊頁面路由
在 src/pages/routes.tsx 匯入新檔案:
import workspacePageHandlers from './workspace';在現有的 pageRouter 路由宣告處新增:
pageRouter.get('/workspace', ...workspacePageHandlers);請放在 pageRouter 掛載到上層應用程式之前。國際化入口會處理語言前綴,不需要另外建立 /:locale/workspace 路由。
啟動開發伺服器後,開啟 /zh-Hant/workspace。在頁面中新增工作區連結時,沿用現有導覽元件的設定方式,或使用 getFullPath('/workspace', locale) 產生目前語言的網址。
四、需要管理員權限時如何修改
如果這個頁面是管理工具,可以在同一個檔案中新增匯入:
import { authz } from '@/authz';接著在身分驗證成功後、回傳頁面前新增:
if (!(await authz.isAdmin(c))) {
return c.notFound();
}這裡選擇對一般使用者回傳 404,避免顯示管理入口。如果產品需要明確顯示沒有權限,也可以設計專用的 403 頁面,文案一樣放進語言資源。
這段程式碼是管理頁面的延伸做法。一般使用者的工作區不需要加入管理員檢查。付費功能的判斷方式請見為業務功能串接付費權益。
五、頁面和 API 分別保護
工作區後續透過 API 讀取資料時,API 仍需獨立檢查身分。使用者可以略過頁面直接呼叫 API,頁面上的檢查不會自動保護另一條路由。
API 沿用相同的 Guard,但回傳 JSON 和狀態碼:
import { gResultCode } from '@/errors';
// 放在 API handler 內,這裡的 c 是目前請求的執行脈絡。
const guard = authenticatedGuard(c);
if (!guard.success) {
return c.json(
guard,
guard.code === gResultCode.authLoginRequired ? 401 : 403,
);
}
const { authContext } = guard.data;
const userId = authContext.userId;userId 來自伺服器端的工作階段。讀取個人資料時,將它傳給業務 Service,不要以接收到的前端使用者 ID 作為目前身分。API 如何串接後續各層,會在下一篇完整實作。
需要瀏覽器互動的表單或清單,應沿用現有的用戶端元件註冊和 SSRWrapper 水合方式。直接在伺服器端頁面中撰寫點擊事件,不會讓頁面自動具備用戶端互動能力。
六、驗證頁面行為
依序檢查下列情況:
| 情況 | 預期結果 |
|---|---|
| 尚未登入就直接開啟工作區 | 進入登入流程 |
| 專案要求驗證電子郵件,使用者尚未驗證 | 進入電子郵件驗證流程 |
| 使用者的 2FA 狀態尚未符合要求 | 進入 Guard 指定的設定或驗證流程 |
| 已完成身分驗證 | 顯示工作區標題和說明 |
| 切換到已啟用的其他語言 | URL 和文案隨語言切換 |
| 一般使用者存取管理頁面的延伸版本 | 回傳 404 |
電子郵件與 2FA 的測試情境,以你啟用的設定和帳號狀態為準。不要為了讓頁面通過檢查而關閉既有的身分驗證要求。
完成後執行 npm run lint、npm run typecheck 和 npm run build。這三個指令分別檢查程式碼規範、型別和建置,無法互相取代。
常見問題包括路由尚未註冊、語言鍵放錯層級,或將所有身分驗證失敗的情況都重新導向登入頁面。遇到問題時先檢查這三處,再參考使用者身分驗證與帳號中的工作階段說明。