Files
pangolin/web/usercenter/lib/api/session.ts
T
wangjia 5b89de656e
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 25s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 7s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 26s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 11s
ci-pangolin / Go — build + test (pull_request) Successful in 15s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 26s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Successful in 10s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Successful in 4m36s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 20s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 10m3s
Deploy Site / deploy-site (push) Failing after 13m20s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 14m42s
ci-pangolin / OpenAPI Sync Check (pull_request) Failing after 14m52s
feat(web): 5 项 UI 修复 — 统一浅色/登录回跳/logo locale/用户名下拉/Docs 真页
用户中心:
- 统一浅色:theme.tsx 无偏好时不再跟随系统 prefers-color-scheme:dark(登录页变
  黑真因),恒浅色;保留手动切换。
- 登录回跳:UserCenter 加 safeRedirect 白名单,登录成功按 ?redirect= 回来源页
  (官网带 /,登录后回主页),无则进 overview。
- logo locale:新增 i18n brandName(zh 穿山甲/其余 Pangolin),Login/UserCenter
  /Subscription 三处引用。
- 存 pg_uc_email(getMe 时)/ clearSession 删,供官网读用户名。

官网(全走 CSS class 合 CSP,无内联 style):
- logo locale:i18n nav.brand(zh 穿山甲/其余 Pangolin),Header+Footer。
- 登录态头部显示用户名(读同源 pg_uc_email)+ 下拉菜单(进入用户中心/切换用户/
  退出登录,复用 .langmenu 风格);未登录 Log in 带 ?redirect=/ 回跳。
- Docs 四卡片补真内容页(en+zh:quickstart/faq/protocol/privacy + Doc.astro
  布局),卡片改回 <a href>;Protocol 改正 sing-box+REALITY(去 WireGuard)。

验证:同源闸绿 · 两端 build 过 · redline 0 · Header 零内联 style · 无 WireGuard。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:52:04 +08:00

68 lines
1.9 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';
// 登录用户邮箱:同源官网(pangolin website)读取以显示用户名。仅邮箱、非敏感凭证。
const EMAIL_KEY = 'pg_uc_email';
export function setEmail(email: string): void {
if (typeof window === 'undefined' || !email) return;
try {
window.localStorage.setItem(EMAIL_KEY, email);
} catch {
/* ignore quota / privacy mode */
}
}
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);
window.localStorage.removeItem(EMAIL_KEY);
} catch {
/* ignore */
}
}
}
export function hasRefresh(): boolean {
return !!getRefreshToken();
}