e51666c52e
- 新增 web_sessions 表(sqlc):refresh_token 以 AES-256-GCM 落盘,密钥取自 CDROP_SESSION_SECRET(env、不在库内);cookie 仅存不透明随机串,表 id 存其 SHA-256,故 DB 单独泄露既换不出可用 cookie、也解不出 token - /auth/exchange 建 session 并下发 HttpOnly; Secure; SameSite=Lax; Path=/api/auth cookie,响应体不再回传 refresh_token - /auth/refresh 改为 cookie 驱动(7 天滑动失活),同时即开机静默免重登; 新增 /auth/logout(删 session + 清 cookie)、/auth/device(写设备名入 session) - device_name 随 session 存,开机 refresh 带回前端,抗 iOS PWA 存储清除; setup / 改名时 syncWebDeviceName 推送服务端 - striped per-session 锁串行化同会话并发 refresh,防一次性 refresh_token 被 花两次而把用户从所有 tab 登出 - 前端 store 移除 refreshToken 字段(长寿命凭据彻底不进 JS);main.tsx 首屏前 bootstrapAuth 用 cookie 静默续期,命中直接进已登录态、避免登录页闪现 - config:prod 强制 CDROP_SESSION_SECRET,缺失拒启动 - 桌面端不受影响(自有 loopback flow + Go keyring,不碰这两个端点)
152 lines
5.8 KiB
TypeScript
152 lines
5.8 KiB
TypeScript
import "@mantine/core/styles.css";
|
||
import "@mantine/dropzone/styles.css";
|
||
import "./styles/index.css";
|
||
|
||
import React, { useEffect } from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import {
|
||
MantineProvider,
|
||
createTheme,
|
||
type MantineColorsTuple,
|
||
} from "@mantine/core";
|
||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||
import { ToastViewport } from "./ui/feedback";
|
||
import { routeTree } from "./routeTree.gen";
|
||
import { useAppStore, type ThemeMode } from "./store";
|
||
import { bootstrapAuth } from "./features/auth/auth";
|
||
import { startCjkAutospace } from "./utils/cjkAutospace";
|
||
import { initDesktopMenuBridge, isDesktop, loadDesktopSettings } from "./net/desktop";
|
||
|
||
// Mantine 需要 10 阶静态色,硬编码值与 tokens.semantic.css 的 --accent-* 阶
|
||
// 严格对齐(Dracula Purple 谱)。组件颜色仍读 var(--accent),这里仅满足
|
||
// Mantine 内部 primaryColor 索引。
|
||
//
|
||
// primaryShade = { light: 7, dark: 4 }:
|
||
// - light 取 [7] = #644AC9(Alucard Purple)
|
||
// - dark 取 [4] = #AA99FF(Dracula Base BrightBlue,Purple 的官方亮版,
|
||
// 在深色背景上对比度 6.7:1,作链接/选中文字可读性达 AAA Large)
|
||
const accentTuple: MantineColorsTuple = [
|
||
"#F3F0FF",
|
||
"#E4DEFF",
|
||
"#C9BEFF",
|
||
"#AC9BFE",
|
||
"#AA99FF",
|
||
"#9580FF",
|
||
"#7458EE",
|
||
"#644AC9",
|
||
"#523CA8",
|
||
"#3F2D85",
|
||
];
|
||
|
||
const theme = createTheme({
|
||
primaryColor: "accent",
|
||
primaryShade: { light: 7, dark: 4 },
|
||
defaultRadius: "md",
|
||
radius: { sm: "6px", md: "10px", lg: "14px" },
|
||
fontFamily: "var(--font-sans)",
|
||
fontFamilyMonospace: "var(--font-mono)",
|
||
headings: { fontFamily: "var(--font-sans)" },
|
||
colors: { accent: accentTuple },
|
||
});
|
||
|
||
const router = createRouter({ routeTree, defaultPreload: "intent" });
|
||
|
||
// 桌面端:菜单栏「设置」→ 路由到 /settings(浏览器里为 no-op)。
|
||
initDesktopMenuBridge(() => { void router.navigate({ to: "/settings" }); });
|
||
// 桌面端:启动即加载本地配置,把剪贴板同步开关应用到门控(浏览器里为 no-op)。
|
||
void loadDesktopSettings();
|
||
|
||
// Cmd+,(macOS)/ Ctrl+,(Windows·Linux)→ 打开设置,桌面与浏览器通用。桌面端
|
||
// 原生菜单未占用 ⌘,,故按键会落到 WebView 由此处理;未登录时 /settings 守卫会
|
||
// 自行重定向,无需额外判断。
|
||
window.addEventListener("keydown", (e) =>
|
||
{
|
||
if ((e.metaKey || e.ctrlKey) && !e.altKey && e.key === ",")
|
||
{
|
||
e.preventDefault();
|
||
void router.navigate({ to: "/settings" });
|
||
}
|
||
});
|
||
|
||
declare module "@tanstack/react-router"
|
||
{
|
||
interface Register
|
||
{
|
||
router: typeof router;
|
||
}
|
||
}
|
||
|
||
// system 模式:移除 data-theme attribute,让 tokens.semantic.css 中的
|
||
// @media (prefers-color-scheme: dark) 接管;light / dark 时写死 attribute。
|
||
function applyTheme(mode: ThemeMode): void
|
||
{
|
||
const root = document.documentElement;
|
||
if (mode === "system") { root.removeAttribute("data-theme"); }
|
||
else { root.setAttribute("data-theme", mode); }
|
||
}
|
||
|
||
// 切语言时给 RouterProvider 换 key 强制全树重挂载,让所有 t() 调用以新 locale
|
||
// 重新求值;t() 仍是纯函数,避免改造每个调用点。这样比 location.reload() 安全
|
||
// (内存里的 refresh_token 不丢)。
|
||
function App()
|
||
{
|
||
const locale = useAppStore((s) => s.locale);
|
||
const theme$ = useAppStore((s) => s.theme);
|
||
|
||
useEffect(() => { applyTheme(theme$); }, [ theme$ ]);
|
||
|
||
// Mantine 自身也维护 colorScheme(决定 [data-mantine-color-scheme])。
|
||
// light / dark 用 forceColorScheme 锁死;system 时不传 force,让其
|
||
// 内部跟随 prefers-color-scheme。store.theme 作为单一来源同步双方。
|
||
const force = theme$ === "system" ? undefined : theme$;
|
||
|
||
return (
|
||
<MantineProvider
|
||
theme={theme}
|
||
defaultColorScheme="auto"
|
||
forceColorScheme={force}
|
||
>
|
||
<ToastViewport />
|
||
<RouterProvider key={locale} router={router} />
|
||
</MantineProvider>
|
||
);
|
||
}
|
||
|
||
// 首屏在 React 挂载前先应用一次主题,避免 FOUC。桌面壳的登录态已在 store 初始化
|
||
// 时从注入的 window.__CDROP_BOOT__ 同步水合(store/helpers.ts),无需异步等待。
|
||
applyTheme(useAppStore.getState().theme);
|
||
|
||
function mount(): void
|
||
{
|
||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||
<React.StrictMode>
|
||
<App />
|
||
</React.StrictMode>,
|
||
);
|
||
// CJK 自动间距 shim:在 React 完成首次挂载后启动。MutationObserver 后续
|
||
// 增量处理 React 重渲染产生的新文本节点。放在 createRoot 之后,首帧绘制
|
||
// 完会触发一次 runShim() 处理已挂载的子树。
|
||
requestAnimationFrame(() => { startCjkAutospace(); });
|
||
}
|
||
|
||
// 浏览器端「免重登」:首屏渲染前用 HttpOnly cookie 尝试静默续期。命中则直接以已
|
||
// 登录态挂载,避免登录页 / 设置页闪现;未命中(401)即照常落到登录页。桌面 / dev /
|
||
// sessionStorage 已有令牌时 bootstrapAuth 立即返回,几乎无延迟。4s 超时兜底:万一
|
||
// refresh 卡死,也不至于把整个应用永久挡在首屏外。
|
||
const BOOT_AUTH_TIMEOUT_MS = 4000;
|
||
void Promise.race([
|
||
bootstrapAuth().catch(() => { /* ignore:失败即落登录页 */ }),
|
||
new Promise<void>((resolve) => { window.setTimeout(resolve, BOOT_AUTH_TIMEOUT_MS); }),
|
||
]).finally(mount);
|
||
|
||
// PWA service worker:仅在浏览器 + prod 注册。dev 下会干扰 Vite HMR;桌面壳
|
||
// (Wails WebView,wails:// / loopback origin)已是原生应用且 SW 行为不可靠,
|
||
// 故跳过。SW 只缓存静态壳、绝不拦截 /api(SSE / 剪贴板 / 中继),见 public/sw.js。
|
||
if (import.meta.env.PROD && !isDesktop() && "serviceWorker" in navigator)
|
||
{
|
||
window.addEventListener("load", () =>
|
||
{
|
||
void navigator.serviceWorker.register("/sw.js").catch(() => {});
|
||
});
|
||
}
|