diff --git a/web/usercenter/.env.example b/web/usercenter/.env.example new file mode 100644 index 0000000..f09b927 --- /dev/null +++ b/web/usercenter/.env.example @@ -0,0 +1,6 @@ +# 数据层开关:mock(演示数据,UI 验收)| http(真实后端,阶段 B) +NEXT_PUBLIC_API_MODE=mock + +# 阶段 B:API 域名池(逗号分隔,N>=3,互不关联)。HttpClient 按序退避重试。 +# 与 doc/05 §1「API 域名池」灾备模型一致;mock 模式下忽略。 +NEXT_PUBLIC_API_DOMAINS=https://api1.example.com,https://api2.example.com,https://api3.example.com diff --git a/web/usercenter/.eslintrc.json b/web/usercenter/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/web/usercenter/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/web/usercenter/.gitignore b/web/usercenter/.gitignore new file mode 100644 index 0000000..8f29978 --- /dev/null +++ b/web/usercenter/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.next/ +out/ +*.tsbuildinfo +next-env.d.ts +.env*.local diff --git a/web/usercenter/README.md b/web/usercenter/README.md new file mode 100644 index 0000000..6289137 --- /dev/null +++ b/web/usercenter/README.md @@ -0,0 +1,78 @@ +# 穿山甲 · Web 用户中心(Next.js 静态导出) + +`design/ui_kits/usercenter/` 设计稿的生产实现。Next.js App Router + `output: 'export'` +**纯静态导出**(无任何 Node 服务端能力),动态数据全走客户端 API,可任意静态托管直接跑。 + +## 页面 +概览 · 订阅导入 · 兑换 & 购买 · 邀请返利 · 设置(语言/主题 + 设备管理 + TOTP 2FA)。 +明/暗主题 × 中/英单显四态,移动端底部 Tab + 左右滑动切换。 + +## 运行 + +```bash +cp .env.example .env.local # 默认 NEXT_PUBLIC_API_MODE=mock +npm install +npm run dev # http://localhost:3000 +``` + +## 构建(静态导出 → out/) + +```bash +npm run redline # 铁律 13 红线词扫描(CI 红线) +npm run build # next build + 导出 + 注入 SRI +# 产物在 out/,可直接静态托管(Cloudflare Pages 多镜像,同官网管线) +``` + +`npm run check` = 红线扫描 + 构建。 + +## 数据层(mock / http 双实现) + +`lib/api/` 抽象 `ApiClient`,构建期由 `NEXT_PUBLIC_API_MODE` 切换实现: + +- **MockClient**(默认):演示数据,零网络依赖,供阶段 A 的 UI 验收。 + - 演示触发:密码 `wrong`→凭证错误,`locked`→账户锁定+倒计时;兑换码 `INVALID`/`USED`/`LOCKED`→对应错误;TOTP 码 `000000`→验证失败。 +- **HttpClient**(阶段 B):真实后端薄客户端 —— API 域名池故障转移 + 指数退避重试 + + 统一错误体 `{code, message_zh, message_en}` 解析 + 401 静默续期重放。 + 域名池由 `NEXT_PUBLIC_API_DOMAINS`(逗号分隔,N≥3)配置。 + +接口:`login / loginTotp(登录二段式)/ refresh / getMe / getSubscription / +resetSubscription / listDevices / removeDevice / redeem / totpSetup / totpVerify / +totpDisable / logout`。 + +> **契约增补(依赖 #1 openapi)**:v1 暂无 TOTP 端点,已在 `lib/api/http.ts` 占位 +> `POST /v1/me/totp/setup|verify|disable` + 登录二段式 `/v1/auth/login/totp`。增补合入前 +> 2FA 仅 mock 验收,不阻塞本任务。 + +## 会话与安全 + +- **access token 仅存内存**(`lib/api/session.ts`),不落盘,降低 XSS 持久化窃取面。 +- **refresh token 走 header token + localStorage**,静默续期(401 自动 refresh 重放一次)。 + - 取舍说明:doc/05 §2 给「HttpOnly+Secure cookie 或 header token」两选。纯静态导出**无服务端 + 设置 cookie 的能力**,故取 header token 方案,配合内存 access + 登出使后端 refresh 白名单失效收敛风险。 +- **登出**调用 `POST /v1/auth/logout` 使 refresh 失效,并清本地令牌。 +- **登录限流在服务端**;前端只做失败提示与锁定倒计时。 + +## 红线 / 合规 + +- **SRI**:构建期 `scripts/add-sri.mjs` 为导出 HTML 的本地 `