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.
106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
/**
|
||
* Apollo Client(v3.0 P0-3,ARCHITECTURE.md §3.4 V3-A2 / §5.1 / §5.2)
|
||
*
|
||
* 双端使用:
|
||
* - 客户端:HttpLink 指向同域 `/api/graphql`,JWT 由 httpOnly cookie 经代理注入
|
||
* - 服务端(RSC):直连 `APOLLO_ROUTER_URL`(内网,由 middleware 注入身份头)
|
||
*
|
||
* APQ(Automatic Persisted Queries,v2.1 M3 安全加固):
|
||
* - 生产环境前端只发 query hash(sha256),不发明文 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 模式(直连 router),false/省略表示客户端(走 /api/graphql)
|
||
*
|
||
* 客户端不再接受 getAuthToken 参数:JWT 已迁至 httpOnly cookie,
|
||
* JS 永远拿不到 token(ARCHITECTURE.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-5):SSR 端也走同域 /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)
|
||
// 客户端不再需要 authLink(token 注入由 /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;
|
||
}
|