nsgm-cli 2.1.10 → 2.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -57
- package/client/redux/store.ts +13 -0
- package/client/redux/template/manage/actions.ts +8 -28
- package/client/service/template/manage.ts +8 -8
- package/client/utils/common.ts +2 -2
- package/client/utils/fetch.ts +371 -36
- package/generation/README.md +68 -28
- package/lib/index.js +34 -1
- package/lib/server/csrf.d.ts +17 -0
- package/lib/server/csrf.js +99 -0
- package/lib/tsconfig.build.tsbuildinfo +1 -1
- package/next.config.js +6 -0
- package/package.json +10 -2
- package/pages/login.tsx +17 -10
- package/pages/template/manage.tsx +35 -27
- package/scripts/generate-password-hash.js +43 -0
- package/server/apis/sso.js +94 -21
package/client/utils/fetch.ts
CHANGED
|
@@ -1,54 +1,389 @@
|
|
|
1
|
+
// GraphQL 客户端与 CSRF 保护工具
|
|
2
|
+
|
|
1
3
|
import axios from 'axios'
|
|
2
4
|
import { getLocalApiPrefix } from './common'
|
|
3
5
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
// 配置 axios 默认行为
|
|
7
|
+
axios.defaults.withCredentials = true
|
|
8
|
+
|
|
9
|
+
// ==================== GraphQL 配置 ====================
|
|
10
|
+
|
|
11
|
+
export const GRAPHQL_CONFIG = {
|
|
12
|
+
// GraphQL 端点
|
|
13
|
+
endpoint: '/graphql',
|
|
14
|
+
|
|
15
|
+
// 默认请求头
|
|
16
|
+
defaultHeaders: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
'Accept': 'application/json'
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// 缓存配置
|
|
22
|
+
cache: {
|
|
23
|
+
defaultTTL: 5 * 60 * 1000, // 5分钟
|
|
24
|
+
maxSize: 100,
|
|
25
|
+
enabled: true
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// CSRF 配置
|
|
29
|
+
csrf: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
tokenHeader: 'X-CSRF-Token',
|
|
32
|
+
cookieName: 'csrfToken'
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// 开发模式配置
|
|
36
|
+
development: {
|
|
37
|
+
enableDebugLogs: process.env.NODE_ENV === 'development'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// GraphQL 操作类型
|
|
42
|
+
export enum GraphQLOperationType {
|
|
43
|
+
QUERY = 'query',
|
|
44
|
+
MUTATION = 'mutation',
|
|
45
|
+
SUBSCRIPTION = 'subscription'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// GraphQL 工具函数
|
|
49
|
+
export const GraphQLUtils = {
|
|
50
|
+
// 检测操作类型
|
|
51
|
+
getOperationType(query: string): GraphQLOperationType {
|
|
52
|
+
const trimmed = query.trim().toLowerCase()
|
|
53
|
+
if (trimmed.startsWith('mutation')) return GraphQLOperationType.MUTATION
|
|
54
|
+
if (trimmed.startsWith('subscription')) return GraphQLOperationType.SUBSCRIPTION
|
|
55
|
+
return GraphQLOperationType.QUERY
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// 提取操作名称
|
|
59
|
+
getOperationName(query: string): string | null {
|
|
60
|
+
const match = query.match(/(?:query|mutation|subscription)\s+(\w+)/)
|
|
61
|
+
return match ? match[1] : null
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// 生成缓存键
|
|
65
|
+
generateCacheKey(query: string, variables?: any): string {
|
|
66
|
+
const operationName = this.getOperationName(query) || 'anonymous'
|
|
67
|
+
const variablesHash = variables ? JSON.stringify(variables) : ''
|
|
68
|
+
return `${operationName}_${btoa(variablesHash)}`
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// 验证 GraphQL 查询语法
|
|
72
|
+
isValidQuery(query: string): boolean {
|
|
73
|
+
try {
|
|
74
|
+
const trimmed = query.trim()
|
|
75
|
+
return (
|
|
76
|
+
trimmed.length > 0 &&
|
|
77
|
+
(trimmed.includes('query') || trimmed.includes('mutation') || trimmed.includes('subscription')) &&
|
|
78
|
+
trimmed.includes('{') &&
|
|
79
|
+
trimmed.includes('}')
|
|
80
|
+
)
|
|
81
|
+
} catch {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ==================== CSRF 工具 ====================
|
|
7
88
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
89
|
+
/**
|
|
90
|
+
* 获取 CSRF token
|
|
91
|
+
* @returns Promise<string> CSRF token
|
|
92
|
+
*/
|
|
93
|
+
export const getCSRFToken = async (): Promise<string> => {
|
|
94
|
+
try {
|
|
95
|
+
const response = await axios.get(getLocalApiPrefix() + '/csrf-token', {
|
|
96
|
+
withCredentials: true
|
|
97
|
+
})
|
|
12
98
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
99
|
+
if (!response.data || !response.data.csrfToken) {
|
|
100
|
+
throw new Error('服务器返回的 CSRF token 为空')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return response.data.csrfToken
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('获取 CSRF token 错误:', error)
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ==================== GraphQL 客户端 ====================
|
|
111
|
+
|
|
112
|
+
// GraphQL 查询缓存
|
|
113
|
+
const queryCache = new Map<string, { data: any; timestamp: number }>()
|
|
114
|
+
const CACHE_DURATION = GRAPHQL_CONFIG.cache.defaultTTL
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* GraphQL 客户端主函数
|
|
118
|
+
* 自动处理 CSRF 保护、缓存、错误重试
|
|
119
|
+
*/
|
|
120
|
+
export const getLocalGraphql = async (query: string, variables: any = {}, useCache = false) => {
|
|
121
|
+
// 验证查询语法
|
|
122
|
+
if (!GraphQLUtils.isValidQuery(query)) {
|
|
123
|
+
throw new Error('Invalid GraphQL query syntax')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 生成缓存键
|
|
127
|
+
const cacheKey = GraphQLUtils.generateCacheKey(query, variables)
|
|
128
|
+
|
|
129
|
+
// 检查缓存
|
|
130
|
+
if (useCache && queryCache.has(cacheKey)) {
|
|
131
|
+
const cached = queryCache.get(cacheKey)!
|
|
132
|
+
if (Date.now() - cached.timestamp < CACHE_DURATION) {
|
|
133
|
+
if (GRAPHQL_CONFIG.development.enableDebugLogs) {
|
|
134
|
+
console.log('GraphQL cache hit:', cacheKey)
|
|
19
135
|
}
|
|
136
|
+
return cached.data
|
|
20
137
|
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// 检测操作类型
|
|
142
|
+
const operationType = GraphQLUtils.getOperationType(query)
|
|
143
|
+
const isMutation = operationType === GraphQLOperationType.MUTATION
|
|
21
144
|
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
145
|
+
const headers: Record<string, string> = {
|
|
146
|
+
...GRAPHQL_CONFIG.defaultHeaders
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let response
|
|
150
|
+
|
|
151
|
+
if (isMutation) {
|
|
152
|
+
// Mutation 使用 POST 方法并需要 CSRF token
|
|
153
|
+
if (GRAPHQL_CONFIG.csrf.enabled) {
|
|
154
|
+
try {
|
|
155
|
+
const csrfToken = await getCSRFToken()
|
|
156
|
+
headers[GRAPHQL_CONFIG.csrf.tokenHeader] = csrfToken
|
|
157
|
+
} catch (csrfError) {
|
|
158
|
+
console.warn('获取 CSRF token 失败,继续执行 GraphQL 请求:', csrfError)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
response = await axios.post(
|
|
163
|
+
getLocalApiPrefix() + '/graphql',
|
|
164
|
+
{
|
|
165
|
+
query,
|
|
166
|
+
variables
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
headers,
|
|
170
|
+
withCredentials: true
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
} else {
|
|
174
|
+
// Query 和 Subscription 使用 GET 方法,不需要 CSRF token
|
|
175
|
+
const params = new URLSearchParams()
|
|
176
|
+
params.append('query', query)
|
|
177
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
178
|
+
params.append('variables', JSON.stringify(variables))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
response = await axios.get(
|
|
182
|
+
getLocalApiPrefix() + '/graphql?' + params.toString(),
|
|
183
|
+
{
|
|
184
|
+
headers: {
|
|
185
|
+
'Accept': 'application/json'
|
|
186
|
+
},
|
|
187
|
+
withCredentials: true
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (response && response.data) {
|
|
193
|
+
// 缓存查询结果
|
|
194
|
+
if (useCache && !isMutation) {
|
|
195
|
+
queryCache.set(cacheKey, {
|
|
196
|
+
data: response.data,
|
|
197
|
+
timestamp: Date.now()
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return response.data
|
|
202
|
+
} else {
|
|
203
|
+
throw new Error('GraphQL response is empty')
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// 只为 mutation 检查 CSRF 错误 (403),因为 query 使用 GET 不需要 CSRF token
|
|
207
|
+
if (axios.isAxiosError(error) && error.response?.status === 403) {
|
|
208
|
+
const operationType = GraphQLUtils.getOperationType(query)
|
|
209
|
+
|
|
210
|
+
if (operationType === GraphQLOperationType.MUTATION) {
|
|
211
|
+
console.warn('🔄 CSRF token 可能已过期,尝试重试 mutation...')
|
|
212
|
+
try {
|
|
213
|
+
// 重新获取 token 并重试 mutation
|
|
214
|
+
const newCsrfToken = await getCSRFToken()
|
|
215
|
+
const retryHeaders = {
|
|
216
|
+
...GRAPHQL_CONFIG.defaultHeaders,
|
|
217
|
+
[GRAPHQL_CONFIG.csrf.tokenHeader]: newCsrfToken
|
|
37
218
|
}
|
|
38
219
|
|
|
39
|
-
|
|
220
|
+
const retryResponse = await axios.post(
|
|
221
|
+
getLocalApiPrefix() + '/graphql',
|
|
222
|
+
{ query, variables },
|
|
223
|
+
{ headers: retryHeaders, withCredentials: true }
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return retryResponse.data
|
|
227
|
+
} catch (retryError) {
|
|
228
|
+
console.error('❌ CSRF mutation 重试失败:', retryError)
|
|
229
|
+
throw retryError
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.error('GraphQL request failed:', error)
|
|
235
|
+
throw error
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ==================== 文件上传工具 ====================
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 创建受 CSRF 保护的文件上传配置
|
|
243
|
+
*/
|
|
244
|
+
export const createCSRFUploadProps = (
|
|
245
|
+
action: string,
|
|
246
|
+
options: {
|
|
247
|
+
name?: string
|
|
248
|
+
onSuccess?: (fileName: string) => void
|
|
249
|
+
onError?: (fileName: string) => void
|
|
250
|
+
beforeUpload?: (file: File) => boolean | Promise<boolean>
|
|
251
|
+
accept?: string
|
|
252
|
+
multiple?: boolean
|
|
253
|
+
} = {}
|
|
254
|
+
) => {
|
|
255
|
+
const {
|
|
256
|
+
name = 'file',
|
|
257
|
+
onSuccess,
|
|
258
|
+
onError,
|
|
259
|
+
beforeUpload: customBeforeUpload,
|
|
260
|
+
accept,
|
|
261
|
+
multiple = false
|
|
262
|
+
} = options
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
name,
|
|
266
|
+
action,
|
|
267
|
+
accept,
|
|
268
|
+
multiple,
|
|
269
|
+
customRequest: async (options: any) => {
|
|
270
|
+
const { onProgress, onError: onUploadError, onSuccess: onUploadSuccess, file } = options
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// 获取 CSRF token
|
|
274
|
+
const csrfToken = await getCSRFToken()
|
|
275
|
+
if (!csrfToken) {
|
|
276
|
+
throw new Error('CSRF Token 获取失败')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 创建 FormData
|
|
280
|
+
const formData = new FormData()
|
|
281
|
+
formData.append(name, file)
|
|
282
|
+
|
|
283
|
+
// 发送请求
|
|
284
|
+
const uploadUrl = action.startsWith('http') ? action : getLocalApiPrefix() + action
|
|
285
|
+
const response = await axios.post(uploadUrl, formData, {
|
|
286
|
+
headers: {
|
|
287
|
+
[GRAPHQL_CONFIG.csrf.tokenHeader]: csrfToken
|
|
288
|
+
},
|
|
289
|
+
withCredentials: true
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
if (response.status >= 200 && response.status < 300) {
|
|
293
|
+
onUploadSuccess(response)
|
|
40
294
|
} else {
|
|
41
|
-
|
|
295
|
+
throw new Error(`Upload failed: ${response.statusText}`)
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
onUploadError(error)
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
beforeUpload: async (file: File) => {
|
|
302
|
+
try {
|
|
303
|
+
// 验证 CSRF token
|
|
304
|
+
const validation = await validateCSRFForUpload()
|
|
305
|
+
if (!validation.valid) {
|
|
306
|
+
throw new Error(validation.error)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 执行自定义的 beforeUpload 检查
|
|
310
|
+
if (customBeforeUpload) {
|
|
311
|
+
const result = await customBeforeUpload(file)
|
|
312
|
+
return result
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return true
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error('Upload preparation failed:', error)
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
onChange(info: any) {
|
|
322
|
+
const { status, name: fileName } = info.file
|
|
323
|
+
|
|
324
|
+
if (status === 'done') {
|
|
325
|
+
if (onSuccess) {
|
|
326
|
+
onSuccess(fileName)
|
|
327
|
+
}
|
|
328
|
+
} else if (status === 'error') {
|
|
329
|
+
if (onError) {
|
|
330
|
+
onError(fileName)
|
|
42
331
|
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
reject(error)
|
|
47
|
-
})
|
|
48
|
-
})
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
49
335
|
}
|
|
50
336
|
|
|
337
|
+
/**
|
|
338
|
+
* 验证上传前的 CSRF 状态
|
|
339
|
+
*/
|
|
340
|
+
export const validateCSRFForUpload = async (): Promise<{ valid: boolean; token?: string; error?: string }> => {
|
|
341
|
+
try {
|
|
342
|
+
const csrfToken = await getCSRFToken()
|
|
343
|
+
if (!csrfToken) {
|
|
344
|
+
return {
|
|
345
|
+
valid: false,
|
|
346
|
+
error: 'CSRF Token 获取失败,请刷新页面重试'
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
valid: true,
|
|
351
|
+
token: csrfToken
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return {
|
|
355
|
+
valid: false,
|
|
356
|
+
error: error instanceof Error ? error.message : '获取 CSRF Token 时发生未知错误'
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ==================== 工具函数 ====================
|
|
362
|
+
|
|
51
363
|
// 清除缓存
|
|
52
364
|
export const clearGraphqlCache = () => {
|
|
53
365
|
queryCache.clear()
|
|
54
366
|
}
|
|
367
|
+
|
|
368
|
+
// GraphQL 查询辅助函数
|
|
369
|
+
export const graphqlQuery = async (query: string, variables?: any, useCache = true) => {
|
|
370
|
+
return getLocalGraphql(query, variables, useCache)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// GraphQL 变更辅助函数 (Mutation)
|
|
374
|
+
export const graphqlMutation = async (mutation: string, variables?: any) => {
|
|
375
|
+
return getLocalGraphql(mutation, variables, false) // 变更操作不使用缓存
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 检查 GraphQL 响应是否有错误
|
|
379
|
+
export const hasGraphqlErrors = (response: any): boolean => {
|
|
380
|
+
return response && response.errors && response.errors.length > 0
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 获取 GraphQL 错误信息
|
|
384
|
+
export const getGraphqlErrorMessage = (response: any): string => {
|
|
385
|
+
if (hasGraphqlErrors(response)) {
|
|
386
|
+
return response.errors.map((error: any) => error.message).join('; ')
|
|
387
|
+
}
|
|
388
|
+
return ''
|
|
389
|
+
}
|
package/generation/README.md
CHANGED
|
@@ -8,34 +8,37 @@
|
|
|
8
8
|
- [Styled-components](https://github.com/styled-components/styled-components) - CSS-in-JS 解决方案
|
|
9
9
|
- [GraphQL](https://graphql.org/) - API 查询语言
|
|
10
10
|
- [MySQL](https://www.mysql.com/) - 关系型数据库
|
|
11
|
+
- 安全登录系统 - 基于 bcrypt 加密
|
|
11
12
|
|
|
12
13
|
## 快速入门
|
|
13
14
|
|
|
14
15
|
### 开发命令
|
|
15
16
|
|
|
16
|
-
| 命令
|
|
17
|
-
|
|
18
|
-
| `npm run dev`
|
|
19
|
-
| `npm run start`
|
|
20
|
-
| `npm run build`
|
|
17
|
+
| 命令 | 说明 |
|
|
18
|
+
| ---------------- | ------------ |
|
|
19
|
+
| `npm run dev` | 开发模式 |
|
|
20
|
+
| `npm run start` | 生产模式 |
|
|
21
|
+
| `npm run build` | 编译项目 |
|
|
21
22
|
| `npm run export` | 导出静态页面 |
|
|
22
23
|
|
|
23
24
|
### 代码生成命令
|
|
24
25
|
|
|
25
|
-
| 命令
|
|
26
|
-
|
|
26
|
+
| 命令 | 说明 |
|
|
27
|
+
| ---------------- | ------------ |
|
|
27
28
|
| `npm run create` | 创建模板页面 |
|
|
28
29
|
| `npm run delete` | 删除模板页面 |
|
|
29
30
|
|
|
30
31
|
### 项目维护命令
|
|
31
32
|
|
|
32
|
-
| 命令
|
|
33
|
-
|
|
34
|
-
| `npm run upgrade`
|
|
33
|
+
| 命令 | 说明 |
|
|
34
|
+
| --------------------------- | ---------------- |
|
|
35
|
+
| `npm run upgrade` | 升级项目基础文件 |
|
|
36
|
+
| `npm run generate-password` | 生成安全密码哈希 |
|
|
35
37
|
|
|
36
38
|
## 参数说明
|
|
37
39
|
|
|
38
40
|
### controller
|
|
41
|
+
|
|
39
42
|
- 用于 `create`/`delete` 命令
|
|
40
43
|
- 必填参数
|
|
41
44
|
- 示例:
|
|
@@ -44,6 +47,7 @@
|
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
### action
|
|
50
|
+
|
|
47
51
|
- 用于 `create`/`delete` 命令
|
|
48
52
|
- 默认值为 `manage`
|
|
49
53
|
- 跟在 controller 参数后面
|
|
@@ -53,6 +57,7 @@
|
|
|
53
57
|
```
|
|
54
58
|
|
|
55
59
|
### dictionary
|
|
60
|
+
|
|
56
61
|
- 用于 `export` 命令
|
|
57
62
|
- 默认值为 `webapp`
|
|
58
63
|
- 示例:
|
|
@@ -88,14 +93,17 @@
|
|
|
88
93
|
const { nextConfig } = require('nsgm-cli')
|
|
89
94
|
const projectConfig = require('./project.config')
|
|
90
95
|
|
|
91
|
-
const { version, prefix, protocol, host } = projectConfig
|
|
96
|
+
const { version, prefix, protocol, host } = projectConfig
|
|
92
97
|
|
|
93
98
|
module.exports = (phase, defaultConfig) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
let configObj = nextConfig(phase, defaultConfig, {
|
|
100
|
+
version,
|
|
101
|
+
prefix,
|
|
102
|
+
protocol,
|
|
103
|
+
host
|
|
104
|
+
})
|
|
97
105
|
|
|
98
|
-
|
|
106
|
+
return configObj
|
|
99
107
|
}
|
|
100
108
|
```
|
|
101
109
|
|
|
@@ -107,13 +115,13 @@ const { mysqlOptions } = mysqlConfig
|
|
|
107
115
|
const { user, password, host, port, database } = mysqlOptions
|
|
108
116
|
|
|
109
117
|
module.exports = {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
mysqlOptions: {
|
|
119
|
+
user,
|
|
120
|
+
password,
|
|
121
|
+
host,
|
|
122
|
+
port,
|
|
123
|
+
database
|
|
124
|
+
}
|
|
117
125
|
}
|
|
118
126
|
```
|
|
119
127
|
|
|
@@ -127,14 +135,46 @@ const { prefix, protocol, host, port } = projectConfig
|
|
|
127
135
|
const { version } = pkg
|
|
128
136
|
|
|
129
137
|
module.exports = {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
version,
|
|
139
|
+
prefix,
|
|
140
|
+
protocol,
|
|
141
|
+
host,
|
|
142
|
+
port
|
|
135
143
|
}
|
|
136
144
|
```
|
|
137
145
|
|
|
146
|
+
## 安全配置
|
|
147
|
+
|
|
148
|
+
项目集成了安全的登录系统,使用 bcrypt 加密。在部署前请配置登录凭证:
|
|
149
|
+
|
|
150
|
+
### 快速设置
|
|
151
|
+
|
|
152
|
+
1. **生成密码哈希**:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm run generate-password yourSecurePassword
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
2. **创建环境变量文件**:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# 在项目根目录创建 .env 文件
|
|
162
|
+
LOGIN_USERNAME=admin
|
|
163
|
+
LOGIN_PASSWORD_HASH=your_generated_hash_here
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
3. **确保 .env 文件在 .gitignore 中**(已预配置)
|
|
167
|
+
|
|
168
|
+
### 详细安全配置
|
|
169
|
+
|
|
170
|
+
更多安全配置和最佳实践,请参考 [SECURITY.md](./SECURITY.md) 文档。
|
|
171
|
+
|
|
172
|
+
**⚠️ 重要提醒:**
|
|
173
|
+
|
|
174
|
+
- 不要在代码中硬编码密码
|
|
175
|
+
- 不要将 `.env` 文件提交到版本控制系统
|
|
176
|
+
- 定期更换登录密码
|
|
177
|
+
|
|
138
178
|
## 开发指南
|
|
139
179
|
|
|
140
180
|
1. **创建新页面**:使用 `npm run create [controller] [action]` 命令
|
|
@@ -144,4 +184,4 @@ module.exports = {
|
|
|
144
184
|
|
|
145
185
|
## 更多资源
|
|
146
186
|
|
|
147
|
-
更多详细信息,请参考 [NSGM CLI 文档](https://github.com/erishen/nsgm-cli)。
|
|
187
|
+
更多详细信息,请参考 [NSGM CLI 文档](https://github.com/erishen/nsgm-cli)。
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.startExpress = void 0;
|
|
8
|
+
// 加载环境变量
|
|
9
|
+
require('dotenv').config();
|
|
8
10
|
// 仅在开发环境中禁用TLS证书验证
|
|
9
11
|
if (process.env.NODE_ENV === 'development') {
|
|
10
12
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
@@ -23,6 +25,8 @@ const config_1 = __importDefault(require("next/config"));
|
|
|
23
25
|
const generate_1 = require("./generate");
|
|
24
26
|
const lodash_1 = __importDefault(require("lodash"));
|
|
25
27
|
const cors_1 = __importDefault(require("cors"));
|
|
28
|
+
const express_session_1 = __importDefault(require("express-session"));
|
|
29
|
+
const csrf_1 = require("./server/csrf");
|
|
26
30
|
const { resolve } = path_1.default;
|
|
27
31
|
const curFolder = process.cwd();
|
|
28
32
|
const processArgvs = (0, args_1.getProcessArgvs)(2);
|
|
@@ -90,19 +94,48 @@ const startExpress = (options, callback) => {
|
|
|
90
94
|
// 不要因为数据库连接失败就退出,允许应用继续运行
|
|
91
95
|
}
|
|
92
96
|
const server = (0, express_1.default)();
|
|
97
|
+
// 配置 session(CSRF 保护需要)
|
|
98
|
+
server.use((0, express_session_1.default)({
|
|
99
|
+
secret: process.env.SESSION_SECRET || 'nsgm-default-secret-key-change-in-production',
|
|
100
|
+
resave: false,
|
|
101
|
+
saveUninitialized: true, // 改为 true,确保 session 被创建
|
|
102
|
+
name: 'sessionId', // 明确指定 session cookie 名称
|
|
103
|
+
cookie: {
|
|
104
|
+
secure: false, // 开发环境总是使用 false,生产环境再考虑 HTTPS
|
|
105
|
+
httpOnly: true,
|
|
106
|
+
maxAge: 24 * 60 * 60 * 1000, // 24小时
|
|
107
|
+
sameSite: 'lax', // 设置 SameSite 策略
|
|
108
|
+
domain: undefined // 不设置 domain,使用默认
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
// 初始化 CSRF token - 移除全局初始化,让每个端点自己处理
|
|
112
|
+
// server.use(setupCSRFToken)
|
|
93
113
|
server.use(body_parser_1.default.urlencoded({
|
|
94
114
|
extended: false
|
|
95
115
|
}));
|
|
96
116
|
// 支持跨域,nsgm export 之后前后分离
|
|
97
|
-
server.use((0, cors_1.default)(
|
|
117
|
+
server.use((0, cors_1.default)({
|
|
118
|
+
credentials: true, // 允许发送 cookies
|
|
119
|
+
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']
|
|
120
|
+
}));
|
|
98
121
|
server.use(body_parser_1.default.json());
|
|
122
|
+
// 添加基本安全中间件
|
|
123
|
+
server.use(csrf_1.securityMiddleware.basicHeaders);
|
|
124
|
+
if (process.env.NODE_ENV === 'production') {
|
|
125
|
+
server.use((0, csrf_1.createCSPMiddleware)()); // 内容安全策略
|
|
126
|
+
}
|
|
127
|
+
// 添加 CSRF 保护中间件(在解析 body 之后)
|
|
128
|
+
server.use(csrf_1.csrfProtection);
|
|
99
129
|
server.use((0, express_fileupload_1.default)());
|
|
100
130
|
server.use('/static', express_1.default.static(path_1.default.join(__dirname, 'public')));
|
|
101
131
|
server.use('/graphql', (0, graphql_1.default)(command));
|
|
102
132
|
const nextConfig = (0, config_1.default)();
|
|
103
133
|
const { publicRuntimeConfig } = nextConfig;
|
|
104
134
|
const { host, port, prefix } = publicRuntimeConfig;
|
|
135
|
+
// 提供 CSRF token 的端点
|
|
136
|
+
server.get('/csrf-token', csrf_1.getCSRFToken);
|
|
105
137
|
if (prefix !== '') {
|
|
138
|
+
server.get(`${prefix}/csrf-token`, csrf_1.getCSRFToken);
|
|
106
139
|
server.use(`${prefix}/static`, express_1.default.static(path_1.default.join(__dirname, 'public')));
|
|
107
140
|
server.use(`${prefix}/graphql`, (0, graphql_1.default)(command));
|
|
108
141
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
declare module 'express-session' {
|
|
3
|
+
interface SessionData {
|
|
4
|
+
_csrf?: string;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
declare module 'express-serve-static-core' {
|
|
8
|
+
interface Request {
|
|
9
|
+
csrfToken?: () => string;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export declare const csrfProtection: (req: Request, res: Response, next: NextFunction) => unknown;
|
|
13
|
+
export declare const getCSRFToken: (req: Request, res: Response) => void;
|
|
14
|
+
export declare const securityMiddleware: {
|
|
15
|
+
basicHeaders: import("express").RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
16
|
+
};
|
|
17
|
+
export declare const createCSPMiddleware: () => import("express").RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|