Files
pangolin/web/usercenter/lib/api/session.ts
T
wangjia e2646346a6 feat(web/usercenter): Next.js 用户中心静态导出 + mock/http 双数据层 (tsk_3FIPC8lSnAfJ)
新建 web/usercenter/:Next.js App Router + output:'export' 纯静态导出,
直接复用 design/ui_kits/usercenter React 源码(概览/订阅/兑换/邀请/设置)。

阶段 A(mock,本提交):
- 复刻五大页面,明/暗 × zh/en 四态;colors_and_type.css 原样链入;
  顶栏主题切换 + 语言段控,移动端底部 Tab + 左右滑动切换。
- 设置页新增:偏好(语言/主题) + 设备管理(列表/移除/二次确认+刷新) +
  TOTP 2FA(绑定二维码占位+密钥/验证/解禁),登录二段式 TOTP。
- 数据层 lib/api:ApiClient 抽象 + MockClient/HttpClient 双实现,构建期
  NEXT_PUBLIC_API_MODE 切换;统一错误体 {code,message_zh,message_en} → 双语映射。
- 会话:access token 仅内存,refresh header token + localStorage,静默续期,
  登出失效(取舍:静态导出无服务端 cookie 能力,详见 README)。
- 安全:构建期注入 SRI(sha384);_headers 严格 CSP + 安全基线;
  红线词扫描(铁律13) CI 红线,零命中。

阶段 B(占位待联调):HttpClient 已写好域名池+退避重试+401 续期;
TOTP/登录二段式端点占位待 #1 契约增补;me/redeem/devices 切真实链路依赖 #2/#3/#4。

验证:npm run lint / redline / build 均通过,静态产物 out/ 无服务端依赖。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 15:23:18 +08:00

56 lines
1.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// session.ts — 会话存储
// access token:仅内存(防 XSS 持久化窃取)refresh tokenlocalStorage(header token 方案)。
// 取舍:doc/05 §2 给「HttpOnly+Secure cookie 或 header token」两选;纯静态导出无服务端
// 设置 cookie 的能力,故取 header token + localStorage,并配合静默续期与登出失效收敛风险。
import type { Session } from './types';
const REFRESH_KEY = 'pg_uc_refresh';
let accessToken: string | null = null;
let accessExpiresAt = 0;
export function getAccessToken(): string | null {
return accessToken;
}
export function accessValid(skewMs = 15_000): boolean {
return !!accessToken && Date.now() < accessExpiresAt - skewMs;
}
export function getRefreshToken(): string | null {
if (typeof window === 'undefined') return null;
try {
return window.localStorage.getItem(REFRESH_KEY);
} catch {
return null;
}
}
export function setSession(s: Session): void {
accessToken = s.accessToken;
accessExpiresAt = s.accessExpiresAt;
if (typeof window !== 'undefined') {
try {
window.localStorage.setItem(REFRESH_KEY, s.refreshToken);
} catch {
/* ignore quota / privacy mode */
}
}
}
export function clearSession(): void {
accessToken = null;
accessExpiresAt = 0;
if (typeof window !== 'undefined') {
try {
window.localStorage.removeItem(REFRESH_KEY);
} catch {
/* ignore */
}
}
}
export function hasRefresh(): boolean {
return !!getRefreshToken();
}