nsgm-cli
Version:
A CLI tool to run Next/Style-components and Graphql/Mysql fullstack project
354 lines (306 loc) • 9.66 kB
text/typescript
// GraphQL 客户端与 CSRF 保护工具
import axios from "axios";
import { getLocalApiPrefix } from "./common";
// 配置 axios 默认行为
axios.defaults.withCredentials = true;
// ==================== GraphQL 配置 ====================
export const GRAPHQL_CONFIG = {
// GraphQL 端点
endpoint: "/graphql",
// 默认请求头
defaultHeaders: {
"Content-Type": "application/json",
Accept: "application/json",
},
// 缓存配置
cache: {
defaultTTL: 5 * 60 * 1000, // 5分钟
maxSize: 100,
enabled: true,
},
// CSRF 配置
csrf: {
enabled: true,
tokenHeader: "X-CSRF-Token",
cookieName: "csrfToken",
},
// 开发模式配置
development: {
enableDebugLogs: process.env.NODE_ENV === "development",
},
};
// GraphQL 操作类型
export enum GraphQLOperationType {
QUERY = "query",
MUTATION = "mutation",
SUBSCRIPTION = "subscription",
}
// GraphQL 工具函数
export const GraphQLUtils = {
// 检测操作类型
getOperationType(query: string): GraphQLOperationType {
const trimmed = query.trim().toLowerCase();
if (trimmed.startsWith("mutation")) return GraphQLOperationType.MUTATION;
if (trimmed.startsWith("subscription")) return GraphQLOperationType.SUBSCRIPTION;
return GraphQLOperationType.QUERY;
},
// 提取操作名称
getOperationName(query: string): string | null {
const match = query.match(/(?:query|mutation|subscription)\s+(\w+)/);
return match ? match[1] : null;
},
// 生成缓存键
generateCacheKey(query: string, variables?: any): string {
const operationName = this.getOperationName(query) || "anonymous";
const variablesHash = variables ? JSON.stringify(variables) : "";
return `${operationName}_${btoa(variablesHash)}`;
},
// 验证 GraphQL 查询语法
isValidQuery(query: string): boolean {
try {
const trimmed = query.trim();
return (
trimmed.length > 0 &&
(trimmed.includes("query") || trimmed.includes("mutation") || trimmed.includes("subscription")) &&
trimmed.includes("{") &&
trimmed.includes("}")
);
} catch {
return false;
}
},
};
// ==================== CSRF 工具 ====================
/**
* 获取 CSRF token
* @returns Promise<string> CSRF token
*/
export const getCSRFToken = async (): Promise<string> => {
try {
const response = await axios.get(`${getLocalApiPrefix()}/csrf-token`, {
withCredentials: true,
});
if (!response.data?.csrfToken) {
throw new Error("服务器返回的 CSRF token 为空");
}
return response.data.csrfToken;
} catch (error) {
console.error("获取 CSRF token 错误:", error);
throw error;
}
};
// ==================== GraphQL 客户端 ====================
/**
* GraphQL 客户端主函数
* 自动处理 CSRF 保护、缓存、错误重试
*/
export const getLocalGraphql = async (query: string, variables: any = {}) => {
// 验证查询语法
if (!GraphQLUtils.isValidQuery(query)) {
throw new Error("Invalid GraphQL query syntax");
}
try {
// 检测操作类型
const operationType = GraphQLUtils.getOperationType(query);
const isMutation = operationType === GraphQLOperationType.MUTATION;
const headers: Record<string, string> = {
...GRAPHQL_CONFIG.defaultHeaders,
};
let response;
if (isMutation) {
// Mutation 使用 POST 方法并需要 CSRF token
if (GRAPHQL_CONFIG.csrf.enabled) {
try {
const csrfToken = await getCSRFToken();
headers[GRAPHQL_CONFIG.csrf.tokenHeader] = csrfToken;
} catch (csrfError) {
console.warn("获取 CSRF token 失败,继续执行 GraphQL 请求:", csrfError);
}
}
response = await axios.post(
`${getLocalApiPrefix()}/graphql`,
{
query,
variables,
},
{
headers,
withCredentials: true,
}
);
} else {
// Query 和 Subscription 使用 GET 方法,不需要 CSRF token
const params = new URLSearchParams();
params.append("query", query);
if (variables && Object.keys(variables).length > 0) {
params.append("variables", JSON.stringify(variables));
}
response = await axios.get(`${getLocalApiPrefix()}/graphql?${params.toString()}`, {
headers: {
Accept: "application/json",
},
withCredentials: true,
});
}
if (response?.data) {
return response.data;
} else {
throw new Error("GraphQL response is empty");
}
} catch (error) {
// 只为 mutation 检查 CSRF 错误 (403),因为 query 使用 GET 不需要 CSRF token
if (axios.isAxiosError(error) && error.response?.status === 403) {
const operationType = GraphQLUtils.getOperationType(query);
if (operationType === GraphQLOperationType.MUTATION) {
console.warn("🔄 CSRF token 可能已过期,尝试重试 mutation...");
try {
// 重新获取 token 并重试 mutation
const newCsrfToken = await getCSRFToken();
const retryHeaders = {
...GRAPHQL_CONFIG.defaultHeaders,
[GRAPHQL_CONFIG.csrf.tokenHeader]: newCsrfToken,
};
const retryResponse = await axios.post(
`/api/graphql`,
{ query, variables },
{ headers: retryHeaders, withCredentials: true }
);
return retryResponse.data;
} catch (retryError) {
console.error("❌ CSRF mutation 重试失败:", retryError);
throw retryError;
}
}
}
console.error("GraphQL request failed:", error);
throw error;
}
};
// ==================== 文件上传工具 ====================
/**
* 创建受 CSRF 保护的文件上传配置
*/
export const createCSRFUploadProps = (
action: string,
options: {
name?: string;
onSuccess?: (fileName: string) => void;
onError?: (fileName: string) => void;
beforeUpload?: (file: File) => boolean | Promise<boolean>;
accept?: string;
multiple?: boolean;
} = {}
) => {
const { name = "file", onSuccess, onError, beforeUpload: customBeforeUpload, accept, multiple = false } = options;
const uploadProps: any = {
name,
action,
multiple,
customRequest: async (options: any) => {
const { onError: onUploadError, onSuccess: onUploadSuccess, file } = options;
try {
// 获取 CSRF token
const csrfToken = await getCSRFToken();
if (!csrfToken) {
throw new Error("CSRF Token 获取失败");
}
// 创建 FormData
const formData = new FormData();
formData.append(name, file);
// 发送请求
const uploadUrl = action.startsWith("http") ? action : getLocalApiPrefix() + action;
const response = await axios.post(uploadUrl, formData, {
headers: {
[GRAPHQL_CONFIG.csrf.tokenHeader]: csrfToken,
},
withCredentials: true,
});
if (response.status >= 200 && response.status < 300) {
onUploadSuccess(response);
} else {
throw new Error(`Upload failed: ${response.statusText}`);
}
} catch (error) {
onUploadError(error);
}
},
beforeUpload: async (file: File) => {
try {
// 验证 CSRF token
const validation = await validateCSRFForUpload();
if (!validation.valid) {
throw new Error(validation.error);
}
// 执行自定义的 beforeUpload 检查
if (customBeforeUpload) {
const result = await customBeforeUpload(file);
return result;
}
return true;
} catch (error) {
console.error("Upload preparation failed:", error);
return false;
}
},
onChange(info: any) {
const { status, name: fileName } = info.file;
if (status === "done") {
if (onSuccess) {
onSuccess(fileName);
}
} else if (status === "error") {
if (onError) {
onError(fileName);
}
}
},
};
// 只有当 accept 有值时才添加该属性
if (accept) {
uploadProps.accept = accept;
}
return uploadProps;
};
/**
* 验证上传前的 CSRF 状态
*/
export const validateCSRFForUpload = async (): Promise<{ valid: boolean; token?: string; error?: string }> => {
try {
const csrfToken = await getCSRFToken();
if (!csrfToken) {
return {
valid: false,
error: "CSRF Token 获取失败,请刷新页面重试",
};
}
return {
valid: true,
token: csrfToken,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : "获取 CSRF Token 时发生未知错误",
};
}
};
// ==================== 工具函数 ====================
// GraphQL 查询辅助函数
export const graphqlQuery = async (query: string, variables?: any) => {
return getLocalGraphql(query, variables);
};
// GraphQL 变更辅助函数 (Mutation)
export const graphqlMutation = async (mutation: string, variables?: any) => {
return getLocalGraphql(mutation, variables);
};
// 检查 GraphQL 响应是否有错误
export const hasGraphqlErrors = (response: any): boolean => {
return response?.errors && response.errors.length > 0;
};
// 获取 GraphQL 错误信息
export const getGraphqlErrorMessage = (response: any): string => {
if (hasGraphqlErrors(response)) {
return response.errors.map((error: any) => error.message).join("; ");
}
return "";
};