nsgm-cli
Version:
A CLI tool to run Next/Style-components and Graphql/Mysql fullstack project
354 lines (306 loc) • 9.58 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(
`${getLocalApiPrefix()}/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 ''
}