Files
Commilitia-Drop/web/src/main.tsx
T
admin e51666c52e 浏览器免重登:HttpOnly cookie + 服务端 session 表 + 设备名持久化
- 新增 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,不碰这两个端点)
2026-06-16 00:54:40 +08:00

152 lines
5.8 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.
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] = #644AC9Alucard Purple
// - dark 取 [4] = #AA99FFDracula Base BrightBluePurple 的官方亮版,
// 在深色背景上对比度 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 WebViewwails:// / 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(() => {});
});
}