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
用户中心: - 统一浅色: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>
68 lines
1.9 KiB
TypeScript
68 lines
1.9 KiB
TypeScript
// session.ts — 会话存储
|
||
// access token:仅内存(防 XSS 持久化窃取);refresh token:localStorage(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();
|
||
}
|