新增登录后的业务页面
从多语言文案和页面路由开始,创建一个正确处理登录、邮箱验证和 2FA 的工作台。
这一篇创建 /workspace 工作台。访问者必须完成项目要求的认证步骤才能打开它,未登录、未验证邮箱或尚未完成 2FA 时,进入相应的处理页面。
先确保本地项目能通过 npm run dev 启动,并且可以注册、登录。本篇只建立页面入口,下一篇再为业务添加数据接口。
一、先准备页面文案
在 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-Hans/workspace。在页面中添加工作台链接时,沿用现有导航组件的配置方式,或使用 getFullPath('/workspace', locale) 生成当前语言的地址。
四、需要管理员权限时怎么改
如果这个页面是管理工具,可以在同一文件增加导入:
import { authz } from '@/authz';然后在认证成功后、返回页面前添加:
if (!(await authz.isAdmin(c))) {
return c.notFound();
}这里选择对普通用户返回 404,避免展示管理入口。若产品需要明确显示无权限,也可以设计专门的 403 页面,文案仍放进语言资源。
这段代码是管理页面的变体。普通用户的工作台不需要添加管理员检查。付费功能的判断见为业务功能接入付费权益。
五、页面和 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。这三个命令分别检查代码规范、类型和构建,不能互相替代。
常见问题是路由没有注册、语言键放错层级,或者把所有认证失败都重定向到登录页。遇到问题先检查这三处,再参考认证与账号中的会话说明。