dotnet-starter-kit 租户端 Dashboard 前端工程规范详解:权限拉取、SSE/Realtime 双通道与手写表单
2026/9/17 5:57:31 网站建设 项目流程

dotnet-starter-kit 租户端 Dashboard 前端工程规范详解:权限拉取、SSE/Realtime 双通道与手写表单

【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API + React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200+ Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit

本文是 dotnet-starter-kit 仓库中.agents/rules/frontend/dashboard.md开发规范的完整解读与源码级展开。该规范面向clients/dashboard这一租户端(Tenant-facing)应用,是开发者在该目录下做任何 React 改动前必须阅读的"分歧清单"——它假定你已经读懂了适用于 admin/dashboard 两个前端应用的公共规范 shared.md,因此只记录 dashboard 独有的约定。读完本文,你将掌握:dashboard 的端口/环境变量/HTTPS 代理设计意图、权限为什么"拉取而非内嵌 JWT"以及如何防闪烁、SSE 两步令牌与双 Context 拆分的实现原理、跨应用模拟登录(Impersonation)的令牌换手机制、chroma 0 中性色主题体系,以及新增页面的完整差异步骤。

一、定位:dashboard 是什么,与 admin 的分工

dashboard 是仓库clients/dashboard目录下的 React 19 + Vite 7 + TypeScript 应用,服务于租户(tenant)——即实际使用产品的企业客户;而clients/admin是运营方(operator)的后台。两者的技术栈底座相同(TanStack Query v5、React Router 7、Radix UI、Tailwind v4、@microsoft/signalr),但设计语言与权限模型刻意不同(详见下文"主题"与"权限"两节)。

两者差异的权威对照表见 shared.md 的"Design language"小节:admin 使用冷色调中性色(hue 240 带少量彩度)与单一 chartreuse 强调色,而 dashboard 使用chroma 0 无色调中性色与可切换的 rose 品牌色——"中性色必须保持 chroma 0"是 dashboard 专属规则,修改时须按所在文件判断归属。

二、端口、环境变量与 HTTPS 开发代理

2.1 端口 5174 与代理目标

  • 端口5174clients/dashboard/vite.config.tsserver.portstrictPort: true,与 admin 的端口区分开,允许两个应用并存开发)。
  • 开发代理目标https://localhost:7030(HTTPS),且对/api配置了ws: true——这是 SignalR Hub 的 WebSocket 升级所必需的,否则/api/v1/realtime/hub的 negotiate 虽然能成功,但 WS 升级会落入 Vite 自己的 dev server,导致聊天状态永远停在 "CONNECTING"。
  • localStorage 前缀fsh.dashboard.*(admin 为fsh.admin.*),这是两者能并排运行的前提。
  • 登录请求头X-FSH-App: dashboard,服务端据此拒绝 root 租户在租户端登录。

2.2 为什么开发代理刻意用 HTTPS

规范明确指出:开发代理使用 HTTPS 是有意为之——如果走 HTTP→HTTPS 的 307 重定向,Authorization承载令牌会在重定向过程中被剥离。因此 dev proxy 直接changeOrigin: true, secure: false指向https://localhost:7030,保证 bearer token 直达 API。

2.3 运行时环境变量(src/env.ts

dashboard 的运行时配置只有三个核心字段:{ apiBase, defaultTenant, demoMode }。阅读 env.ts 可以看到,它比 shared 规范中描述的多继承了inactivityIdleMs(默认 20 分钟)与inactivityWarningMs(默认 60 秒)两个非活动超时参数,并通过positiveOr()做防御性取值(要求正有限数,否则回退默认值)。

关键的架构决策是:环境变量是运行时加载而非构建时注入loadRuntimeConfig()main.tsx挂载 React 前await一次/config.jsonenv是 getter,过早读取会抛错。这意味着同一个构建产物可以跨环境直接晋升——运维只需改写config.json,不需要重新构建镜像。dashboard 的vite.config.ts中还内置了一个名为fsh-dev-direct-api-config的开发插件:dev 模式下直接改写/config.json的响应,把apiBase指向https://localhost:7030,让 REST 请求与长连接的 SSE/SignalR 流绕过 Vite 代理直连 API——否则这些长连接占用 localhost:5174 的 HTTP/1.1 每主机约 6 条连接上限,会间歇性饿死懒加载的路由 chunk(注释里直接写了 "page won't load" 的教训)。

生产环境的clients/dashboard/public/config.json保持apiBase: ""(同源默认)。

三、表单:拒绝 RHF/zod,手写受控组件

dashboard 的硬性约定:不依赖 react-hook-form 或 zod,不要为了与 admin 对齐而引入这两个依赖。表单一律使用受控输入(controlled inputs)+ 组件本地 state 手写。

这是与 admin 的明确分歧:admin 允许/使用 RHF 风格的表单方案,dashboard 刻意保持轻量。在新增页面时(见第八节),表单部分必须遵循此约定。

四、权限体系:从接口拉取而非内嵌 JWT

这是 dashboard 与 admin 在权限模型上的核心差异,也是规范着墨最多的部分。

4.1 JWT 只携带角色名

dashboard 的 JWT只包含角色名(role names),不包含权限列表。权限的权威来源是服务端按角色解析后的接口:

  • GET /api/v1/identity/permissions,封装为src/api/identity.ts中的getMyPermissions()(见 identity.ts 中getMyPermissions的实现,注释明确写着 "The JWT carries only role names; permissions are resolved server-side per role here")。

4.2 拉取、缓存与防闪烁

auth-context.tsx(见 auth-context.tsx)中的AuthProvider签名主体(subject)变化时——包括冷启动、登录、模拟登录切换——都会触发权限拉取:

  1. 调用getMyPermissions()
  2. 结果写入tokenStore.setPermissions(perms),缓存于 localStorage 的fsh.dashboard.permissions键(见 token-store.ts 中getPermissions/setPermissions,带 JSON 解析防御);
  3. 置位permissionsHydrated标志。

permissionsHydrated的存在意义是:避免受权限门控的 UI 在请求进行中闪现。初始 state 会从缓存权限列表种子化(tokenStore.getPermissions().length > 0即视为已水合),因此热刷新不会闪未门控的 UI。拉取失败不会登出用户——门控 UI 保持隐藏直到下次成功水合。login()时还会先清空权限缓存再发令牌,防止上一个用户的权限泄漏进新会话。

此外还有refreshPermissions()暴露给 AuthContext,用于角色变更后主动刷新权限集。

4.3 导航门控:perm/anyPerm

导航项在 nav-data.ts 中通过NavSpecperm(单一权限,AND 语义)与anyPerm(任选其一即可)字段门控:

// perm:必须持有 Permissions.Chat.Channels.View 才显示 { to: "/chat", label: "Chat", icon: MessageCircle, perm: "Permissions.Chat.Channels.View" }, // anyPerm:Trash 有五个页签、各门控在不同资源的 restore/view-trash 权限上, // 只要用户能进任何一个页签就显示入口 { to: "/system/trash", label: "Trash", icon: Trash2, anyPerm: ALL_TRASH_PERMISSIONS },

isNavItemVisible()实现"perm AND anyPerm"的判定,visibleSections()/visibleItems()负责过滤并丢弃空 section。一个值得注意的细节:/identity/users|roles|groups门控在*.Update而非*.View——因为 View 属于 IsBasic 权限、每个成员都持有(聊天/用户选择器依赖 Users.View),只有管理员级别的用户才应看到管理页面。

4.4 路由守卫仍是"仅认证"

与 admin 不同,dashboard 的ProtectedRoute只做认证检查,不做逐路由权限门控——规范明确要求不要在此引入 admin 那种RouteGuard风格的门控。导航门控(隐藏入口)+ 服务端 403(API 层拦截)构成了双层防线,前端不再画蛇添足。

五、路由与实时通道:SignalR + 专属 SSE

5.1 路由与懒加载

  • 每个路由元素都用withSuspense(node)包裹(逐路由骨架屏 fallback),不设逐路由权限守卫
  • Provider 挂载顺序:RealtimeProviderSseProvider都挂在AppShell内部(仅认证路由),外层再套CommandPaletteProvider(cmdk 命令面板)。
  • 页面全部为命名导出,通过lazyNamed(importer, name)适配为React.lazy,见 shared 规范。

5.2 SignalR(src/realtime/realtime-context.tsx

  • 单一共享的HubConnection连接到/api/v1/realtime/hub,预接线约 11 个聊天/通知事件。
  • @microsoft/signalr动态导入的(约 37KB gzip,全 shell 中最重的单依赖),只在已认证会话打开 hub 时才拉取——settings/files/health/auth 等无实时消费者的页面永远不会下载它(见 realtime-context.tsx 中loadSignalR()的单例 Promise 设计)。
  • 认证走accessTokenFactorytokenEpochtokenStore.subscribe递增,任何登录/刷新/模拟登录切换都会强制重建连接,避免旧令牌的僵尸连接。
  • 重连采用递归退避[2s, 5s, 10s, 30s],连续失败上限 60s 并带 ±15% 抖动;传输方式不固定(WebSockets → SSE → long-polling 自动降级),因为企业代理后方的仪表盘常常需要回退。
  • 消费方式:useRealtimeEvent("EventName", handler, deps),handler 存在 ref 中避免闭包过期。

5.3 SSE(src/sse/,dashboard 独有)

SSE 是 dashboard 区别于 admin 的专属能力,采用两步令牌流程:

  1. POST /api/v1/sse/token换取一次性短寿命令牌(见 sse-api.ts 的issueSseToken());
  2. GET /api/v1/sse/stream?token=<guid>通过fetch 流式读取消费(parseSseStream异步生成器,手写解析event:/id:/data:字段与\n\n分隔符)。

为什么不用 EventSource?因为 EventSource 无法发送Authorization请求头,而 SSE 流需要认证。令牌只在校验握手那一刻被检查,流一旦建立,其生命周期由传输层(网络/服务端)决定;断线重连时会重新调用issueSseToken(),而它背后的用户 JWT 会通过 api-client 的 401 单飞刷新自动续期——长会话的令牌刷新被隐式纳入重连路径,无需专用定时器。连接失败采用 1s→30s 的指数退避(INITIAL_BACKOFF_MS = 1000MAX_BACKOFF_MS = 30_000),事件列表上限 200 条。

双 Context 拆分是性能关键设计(见 sse-context.tsx 中注释记录的历史教训):旧版单 Context 的 value 因events每次都是新数组而每次事件都变化,导致整个 overview 树级联重渲染。现在拆成:

  • useSseStatus()——稳定({ status, eventCount }),只适合顶部状态点、铃铛角标这类只关心连接状态的消费者;
  • useSseEvents()——每次事件都变化({ events }),只在真正渲染事件列表的组件中挂载(overview 实时动态、activity 页);
  • useSse()——向后兼容的组合钩子,新代码应改用上面两个按需切片。

六、模拟登录(Impersonation):跨应用单程移交

6.1 令牌藏匿(stash)机制

token-store.ts提供了三个关键方法(见 token-store.ts):

  • beginImpersonation(accessToken, impersonatedTenant):把操作者(operator)的原始 access/refresh/tenant 令牌藏到fsh.dashboard.impersonation.*键下,然后把活动令牌换成模拟登录令牌,并移除 refresh 槽——因为服务端不签发模拟登录的 refresh 令牌,api-client 检测到无 refresh 令牌就会静默跳过自动刷新(模拟登录会话刻意设计为短命);
  • endImpersonationWithFreshTokens(access, refresh):End 成功路径,用服务端为原始操作者新铸的令牌对替换活动令牌并清空藏匿;
  • restoreStashedActor():End 失败时的兜底,本地恢复藏匿令牌(原 access 可能已过期,此时藏匿的 refresh 会触发自动刷新)。

6.2 AuthProvider 暴露的接口与防御性设计

AuthProvider暴露beginImpersonation/stopImpersonation,并从act_sub/act_tenant/act_name声明推导ImpersonationInfo(见 auth-context.tsx 的claimsToImpersonation)。

stopImpersonation分两种情况,逻辑非常讲究:

  • 无藏匿(跨应用移交):说明操作者是 root SuperAdmin 从 admin 端发起的移交,dashboard 端没有可回归的会话,且把 root 账户恢复到租户端正是login()明令禁止的——所以立即登出,服务端endImpersonation只做 best-effort 调用(用于吊销授权 + 审计,30s 超时不容阻塞 UI);
  • 有藏匿(应用内模拟):await 服务端 End 换取操作者新令牌,若新令牌 tenant 仍是 root(防御纵深),同样登出而非恢复。

另外,beginImpersonation/stopImpersonation都会queryClient.clear(),避免 actor 会话的用户/角色/权限缓存泄漏给被模拟者。移交方向是单向的:admin 通过其dashboardUrl触发移交,dashboard 不反向移交(这也是env.ts注释说明 dashboard 不需要dashboardUrl配置的原因)。

6.3 会话恢复的边界

AuthProvider还处理了两个容易踩坑的会话恢复场景:冷启动时 access 过期但 refresh 存在,会先做一次静默刷新(isInitializing期间渲染 loader,而不是闪现注定 401 的仪表盘);以及跨标签页storage事件与visibilitychange监听,确保 DevTools 手动清令牌或另一标签登出后,本标签不会继续用已丢失的令牌发请求。

七、性能与主题

7.1 性能约定

  • @tanstack/react-virtual:任何大集合(聊天历史、大表格)必须用它做虚拟滚动。
  • cmdk:驱动命令面板(CommandPaletteProvider)。

7.2 主题:chroma 0 中性色 + 可换强调色

设计语言定义在 globals.css,遵循"原始值 → 语义变量 →@theme inline工具类"的三层 token 架构:

  • 中性色全部 chroma 0--neutral-*: oklch(L 0 0),无色调)。规范明确指出"暖纸色(warm-paper)被刻意移除"——旧版的暖纸底盘会让每个表面都泛黄,在深色模式下表现为整体黄色滤镜,且与非 rose 的强调色打架。现在中性色完全无色调,让所选强调色成为房间里唯一的颜色。
  • 默认品牌色 rose(600 停靠点#f91942附近的 oklch),通过:root上的.accent-{rose,indigo,violet,sky,emerald,amber}类覆盖全部--brand-*oklch 停靠点实现整套品牌色一键切换
  • saffron 次强调色--saffron-*,暖调第二通道,用于渐变端点、主视觉数字、信任标记)。
  • 字体 Figtree(区别于 admin 的 Geist / Geist Mono)。

新增 token 的流程是:在globals.css中按"原始值 → 语义值 →@theme inline"补全三层,然后使用工具类;禁止在组件里硬编码颜色

八、新增页面:在共享步骤之上的 delta

shared 规范给出了四步通用流程(扩展src/api/{feature}.ts手写类型与apiFetch调用 → 建src/pages/{area}/{name}.tsx命名导出页 → 在AppShell下注册lazyNamed路由 → 写tests/{area}/{name}.spec.ts测试)。dashboard 在此基础上叠加五条差异:

  1. 手写表单(不用 RHF/zod),受控输入 + 本地 state;
  2. 路由元素包withSuspense(<X/>),不设权限守卫(导航门控 + 服务端 403 负责权限);
  3. 若页面消费推送:SignalR 用useRealtimeEvent("EventName", handler)(新事件名须先在 realtime-context.tsx 的预接线事件列表中注册),SSE 用useSseEvents()
  4. 长列表用react-virtual
  5. 保持中性色 chroma 0。

九、测试与验证依据

dashboard 的 Playwright 测试(route-mocked,无真实后端)落在clients/dashboard/tests/{area}/{name}.spec.ts,覆盖 auth、billing、catalog、chat、files、identity、impersonation、overview、settings、system、tickets 各域。测试采用 shared 规范描述的 JWT 种子化(seedAuthedSession构造假 JWT 写入fsh.dashboard.*localStorage)+ shell mock(installShellMocksabort SSE/SignalR)策略,beforeEach统一执行。对于实现细节的验证,最直接的路径是:

  • 权限拉取链:identity.ts 的getMyPermissions→ auth-context.tsx 的水合 effect → token-store.ts 的fsh.dashboard.permissions缓存;
  • SSE 两步令牌:sse-api.ts → sse-context.tsx 的连接循环;
  • 代理与端口:vite.config.ts。

十、总结

clients/dashboard的设计哲学可以概括为几条清晰的取舍:权限走服务端权威(JWT 只带角色、权限单独拉取并缓存)、实时走双通道(SignalR 管高吞吐事件、SSE 管可认证的流式推送)、表单保持原生(不引入重型表单库)、主题保持纯净(chroma 0 中性色 + 可换强调色)。理解这些约定与其背后的历史教训(307 重定向剥 header、HTTP/1.1 连接数饿死懒加载、SSE 单 Context 级联重渲染、跨应用移交的 root 账户防御),是在该租户端应用上高效、合规地新增功能的前提。修改任何 dashboard 代码前,请先通读 shared.md 与本文件,再对照上述源码路径核实行为。

【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API + React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200+ Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询