Files
Edu/apps/portal-shell/src/lib/apollo-client.ts
SpecialX 9358372657 feat(portal-shell): add MSW mock layer with production bundle exclusion (P1-5)
MSW v2.7.0 fallback layer covering dashboard/users/exams/grades domains.
NEXT_PUBLIC_MSW=1 enables browser Service Worker + SSR route handler mock
responses without backend. Production build excludes all mock data via
Turbopack resolveAlias redirecting @/mocks to empty stub.

Acceptance: build bundle (client+server) verified clean of mock strings;
typecheck/lint/vitest (231 tests) all pass.
2026-07-22 14:48:30 +08:00

106 lines
4.0 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.
/**
* Apollo Clientv3.0 P0-3ARCHITECTURE.md §3.4 V3-A2 / §5.1 / §5.2
*
* 双端使用:
* - 客户端HttpLink 指向同域 `/api/graphql`JWT 由 httpOnly cookie 经代理注入
* - 服务端RSC直连 `APOLLO_ROUTER_URL`(内网,由 middleware 注入身份头)
*
* APQAutomatic Persisted Queriesv2.1 M3 安全加固):
* - 生产环境前端只发 query hashsha256不发明文 query
* - apollo-router 通过 hash 查找 pq-manifest.json 中的 query 文本
* - 防止攻击者通过 DevTools 构造任意查询探测 schema
* - 开发模式可设 NEXT_PUBLIC_APOLLO_APQ=false 关闭 APQ 便于调试
*
* 安全V3-A2
* - 客户端不再注入 Authorization 头token 全程不出 httpOnly cookie
* - 客户端不再读 localStorage.edu_token方案已废止
* - 客户端 credentials:"include" 让 cookie 流向同域 /api/graphql
*
* 关联portal-shell ARCHITECTURE.md §3.4 V3-A2、§5.1、§5.2
*/
import { ApolloClient, InMemoryCache, HttpLink, from } from "@apollo/client";
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
import { sha256 } from "crypto-hash";
/**
* 服务端 RSC 直连 apollo-router URL仅服务端可用浏览器走同域代理
* 不挂 NEXT_PUBLIC_ 前缀 → 不打包进客户端 bundle。
*/
const SERVER_APOLLO_ROUTER_URL =
process.env.APOLLO_ROUTER_URL || "http://localhost:3000/graphql";
/**
* 客户端同域代理路径V3-A2浏览器只发同域请求
* 由 /api/graphql Route Handler 取 httpOnly cookie 中的 JWT 并转发。
*/
const CLIENT_PROXY_URL = "/api/graphql";
// 开发模式可关闭 APQ 便于调试NEXT_PUBLIC_APOLLO_APQ=false
// 生产环境默认启用(未设置或设置为 true 均启用)
const APQ_ENABLED = process.env.NEXT_PUBLIC_APOLLO_APQ !== "false";
// MSW 启用时跳过 SSR 查询P1-5浏览器端由 MSW SW 拦截 /api/graphql
const MSW_ENABLED = process.env.NEXT_PUBLIC_MSW === "1";
/**
* 创建 Apollo Client 实例。
*
* @param options 可选:
* - serverSide: true 表示服务端 RSC 模式(直连 routerfalse/省略表示客户端(走 /api/graphql
*
* 客户端不再接受 getAuthToken 参数JWT 已迁至 httpOnly cookie
* JS 永远拿不到 tokenARCHITECTURE.md §3.4 V3-A2 / §11.7 红线 #2
*/
export function createApolloClient(
options: { serverSide?: boolean } = {},
): ApolloClient<unknown> {
const isServer = options.serverSide ?? typeof window === "undefined";
const httpLink = new HttpLink({
// MSW 启用时P1-5SSR 端也走同域 /api/graphql由 Route Handler 返回 mock 数据
uri: isServer && !MSW_ENABLED ? SERVER_APOLLO_ROUTER_URL : CLIENT_PROXY_URL,
// 客户端同域请求cookie 自动随行;服务端:直连 router 不需要 cookie
credentials: isServer ? "omit" : "include",
});
// Link 链顺序pqLink → httpLink
// - pqLink 将 query 替换为 hash启用时
// - httpLink 发送请求(含 cookie
// 客户端不再需要 authLinktoken 注入由 /api/graphql 代理负责)
const link = APQ_ENABLED
? from([createPersistedQueryLink({ sha256 }), httpLink])
: from([httpLink]);
return new ApolloClient({
link,
cache: new InMemoryCache(),
ssrMode: isServer,
defaultOptions: {
query: {
errorPolicy: "all",
fetchPolicy: isServer ? "no-cache" : "cache-first",
},
watchQuery: {
errorPolicy: "all",
},
},
});
}
let clientSingleton: ApolloClient<unknown> | null = null;
/**
* 获取客户端 Apollo Client 单例(浏览器侧复用缓存)。
*
* 注意:不再接受 getAuthToken 参数V3-A2 移除 localStorage 方案)。
*/
export function getApolloClient(): ApolloClient<unknown> {
if (typeof window === "undefined") {
return createApolloClient({ serverSide: true });
}
if (!clientSingleton) {
clientSingleton = createApolloClient({ serverSide: false });
}
return clientSingleton;
}