create-skweb 3.1.1 → 3.3.0
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/.rollup.cache/Users/stayknight/projects/skweb-mono/tools/create-skweb/dist/index.js +14 -1
- package/.rollup.cache/Users/stayknight/projects/skweb-mono/tools/create-skweb/tsconfig.tsbuildinfo +1 -1
- package/.turbo/turbo-build.log +16 -17
- package/.turbo/turbo-lint.log +11 -12
- package/.turbo/turbo-test.log +66 -1
- package/CHANGELOG.md +49 -0
- package/dist/cli.js +14 -1
- package/dist/index.js +14 -1
- package/package.json +1 -1
- package/src/index.ts +21 -1
- package/templates/cjs/.env.example +5 -9
- package/templates/cjs/ecosystem.config.js +2 -2
- package/templates/cjs/package.json +5 -5
- package/templates/cjs/src/controllers/user.js +2 -2
- package/templates/cjs/src/cron-app.js +40 -0
- package/templates/cjs/src/cron-framework.js +44 -0
- package/templates/cjs/src/swagger.js +13 -18
- package/templates/cjs/src/utils/config.js +33 -13
- package/templates/cjs/src/{app.js → web-app.js} +41 -53
- package/templates/cjs/src/web-framework.js +23 -0
- package/templates/mjs/.env.example +5 -9
- package/templates/mjs/ecosystem.config.js +2 -2
- package/templates/mjs/package.json +5 -5
- package/templates/mjs/src/controllers/user.js +2 -2
- package/templates/mjs/src/cron-app.js +45 -0
- package/templates/mjs/src/cron-framework.js +44 -0
- package/templates/mjs/src/swagger.js +13 -18
- package/templates/mjs/src/utils/config.js +29 -13
- package/templates/mjs/src/{app.js → web-app.js} +40 -56
- package/templates/mjs/src/web-framework.js +23 -0
- package/templates/ts/.env.example +5 -9
- package/templates/ts/ecosystem.config.js +2 -2
- package/templates/ts/package.json +5 -5
- package/templates/ts/src/controllers/user.ts +2 -2
- package/templates/ts/src/cron-app.ts +45 -0
- package/templates/ts/src/cron-framework.ts +44 -0
- package/templates/ts/src/swagger.ts +13 -19
- package/templates/ts/src/utils/config.ts +29 -13
- package/templates/ts/src/{app.ts → web-app.ts} +40 -63
- package/templates/ts/src/web-framework.ts +23 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/templates/cjs/src/cronWorker.js +0 -80
- package/templates/cjs/src/index.js +0 -7
- package/templates/cjs/src/services/database.js +0 -15
- package/templates/mjs/src/cronWorker.js +0 -86
- package/templates/mjs/src/index.js +0 -8
- package/templates/mjs/src/services/database.js +0 -16
- package/templates/ts/src/cronWorker.ts +0 -85
- package/templates/ts/src/index.ts +0 -8
- package/templates/ts/src/services/database.ts +0 -16
|
@@ -1,42 +1,21 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
require('dotenv/config')
|
|
2
3
|
|
|
3
4
|
const path = require('path')
|
|
4
|
-
// skweb-express / skweb-express-logger 的 CJS 产物形态是
|
|
5
|
-
// `{ default: fn, errorHandler, ... }`。直接 `require('skweb-express')` 拿到的是
|
|
6
|
-
// 命名空间对象,`express()` 会 TypeError。这里走 .default。
|
|
7
5
|
const express = require('skweb-express').default
|
|
8
6
|
const expressLogger = require('skweb-express-logger').default
|
|
9
7
|
const expressErrorHandler = require('skweb-express-logger').errorHandler
|
|
10
8
|
const swaggerUi = require('swagger-ui-express')
|
|
11
|
-
const {
|
|
9
|
+
const { framework } = require('./web-framework.js')
|
|
12
10
|
const { logger } = require('./utils/logger.js')
|
|
13
|
-
const { checkRequiredEnv } = require('./utils/config.js')
|
|
11
|
+
const { checkRequiredEnv{{#if_eq auth "static"}}, checkJwtSecret{{/if_eq}}{{#if_eq auth "jwks"}}, checkOIDCConfig{{/if_eq}} } = require('./utils/config.js')
|
|
14
12
|
const { buildOpenApiSpec } = require('./swagger.js')
|
|
15
13
|
|
|
14
|
+
{{#if_eq auth "static"}}
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
* - 'static' (默认):使用共享密钥(HS256/HS384/HS512),适用于自签 token
|
|
19
|
-
* - 'jwks':通过 JWKS 端点动态获取公钥(RS256 等),适用于 Keycloak/Auth0 等 IdP
|
|
16
|
+
* JWT 配置(static 模式,HS256 共享密钥)。
|
|
20
17
|
*/
|
|
21
18
|
function buildJwtOptions() {
|
|
22
|
-
const provider = process.env.AUTH_PROVIDER || 'static'
|
|
23
|
-
|
|
24
|
-
if (provider === 'jwks') {
|
|
25
|
-
const issuer = process.env.OIDC_ISSUER
|
|
26
|
-
const jwksUri = process.env.OIDC_JWKS_URI
|
|
27
|
-
const audience = process.env.OIDC_AUDIENCE
|
|
28
|
-
if (!issuer && !jwksUri) {
|
|
29
|
-
throw new Error('AUTH_PROVIDER=jwks requires OIDC_ISSUER or OIDC_JWKS_URI')
|
|
30
|
-
}
|
|
31
|
-
return {
|
|
32
|
-
provider: 'jwks',
|
|
33
|
-
algorithms: ['RS256'],
|
|
34
|
-
jwks: { jwksUri, issuer, audience },
|
|
35
|
-
verify: { issuer, audience },
|
|
36
|
-
requestProperty: 'resolvedToken'
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
19
|
return {
|
|
41
20
|
provider: 'static',
|
|
42
21
|
secret: process.env.JWT_SECRET || 'please-change-this-in-production',
|
|
@@ -45,6 +24,26 @@ function buildJwtOptions() {
|
|
|
45
24
|
requestProperty: 'resolvedToken'
|
|
46
25
|
}
|
|
47
26
|
}
|
|
27
|
+
{{else}}
|
|
28
|
+
/**
|
|
29
|
+
* JWT 配置(jwks 模式,通过 OIDC 兼容的 JWKS 端点验证 RS256 签名)。
|
|
30
|
+
*/
|
|
31
|
+
function buildJwtOptions() {
|
|
32
|
+
const issuer = process.env.OIDC_ISSUER
|
|
33
|
+
const jwksUri = process.env.OIDC_JWKS_URI
|
|
34
|
+
const audience = process.env.OIDC_AUDIENCE
|
|
35
|
+
if (!issuer && !jwksUri) {
|
|
36
|
+
throw new Error('jwks mode requires OIDC_ISSUER or OIDC_JWKS_URI')
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
provider: 'jwks',
|
|
40
|
+
algorithms: ['RS256'],
|
|
41
|
+
jwks: { jwksUri, issuer, audience },
|
|
42
|
+
verify: { issuer, audience },
|
|
43
|
+
requestProperty: 'resolvedToken'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
{{/if_eq}}
|
|
48
47
|
|
|
49
48
|
/**
|
|
50
49
|
* Swagger UI 仅在 NODE_ENV 为 development / test,且 SWAGGER_ENABLED !== 'false' 时启用。
|
|
@@ -64,25 +63,18 @@ async function bootstrap() {
|
|
|
64
63
|
logger.error('[app] Configuration validation failed, exiting...')
|
|
65
64
|
process.exit(1)
|
|
66
65
|
}
|
|
66
|
+
{{#if_eq auth "static"}}
|
|
67
|
+
if (!checkJwtSecret()) {
|
|
68
|
+
logger.error('[app] JWT configuration validation failed, exiting...')
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
{{else}}
|
|
72
|
+
if (!checkOIDCConfig()) {
|
|
73
|
+
logger.error('[app] OIDC configuration validation failed, exiting...')
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
{{/if_eq}}
|
|
67
77
|
|
|
68
|
-
const framework = new WebFramework({
|
|
69
|
-
db: {
|
|
70
|
-
host: process.env.DB_HOST || '127.0.0.1',
|
|
71
|
-
port: Number(process.env.DB_PORT) || 3306,
|
|
72
|
-
username: process.env.DB_USER || 'root',
|
|
73
|
-
password: process.env.DB_PASS || '',
|
|
74
|
-
database: process.env.DB_NAME || 'test-project',
|
|
75
|
-
dialect: process.env.DB_DIALECT || 'mysql',
|
|
76
|
-
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
77
|
-
underscored: true,
|
|
78
|
-
logging: process.env.DEBUG_DB === 'true' ? (msg) => logger.debug(msg) : false
|
|
79
|
-
},
|
|
80
|
-
redis: {
|
|
81
|
-
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
82
|
-
port: Number(process.env.REDIS_PORT) || 6379,
|
|
83
|
-
password: process.env.REDIS_PASSWORD || undefined
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
78
|
const app = framework.createApp()
|
|
87
79
|
|
|
88
80
|
await framework.setup({
|
|
@@ -99,21 +91,19 @@ async function bootstrap() {
|
|
|
99
91
|
app.setOkCode(0)
|
|
100
92
|
app.setOkMessage('success')
|
|
101
93
|
|
|
102
|
-
// JWT 配置(按 AUTH_PROVIDER 选择 static 或 jwks 模式)
|
|
103
94
|
const jwtOptions = buildJwtOptions()
|
|
104
95
|
app.setJWT('default', jwtOptions)
|
|
105
96
|
app.setJwtHintHeader('JwtHint')
|
|
106
97
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
}
|
|
98
|
+
{{#if_eq auth "static"}}
|
|
99
|
+
logger.info('[app] JWT auth provider: static (HS256)')
|
|
100
|
+
{{else}}
|
|
101
|
+
logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks?.issuer || jwtOptions.jwks?.jwksUri})`)
|
|
102
|
+
{{/if_eq}}
|
|
112
103
|
|
|
113
104
|
app.use(expressLogger())
|
|
114
105
|
app.use(express.static(path.join(__dirname, '..', 'public')))
|
|
115
106
|
|
|
116
|
-
// 仅在 dev / test 环境挂载 Swagger UI(默认关闭,需显式 SWAGGER_ENABLED=true)
|
|
117
107
|
if (shouldEnableSwagger()) {
|
|
118
108
|
const openapiSpec = buildOpenApiSpec()
|
|
119
109
|
app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
|
|
@@ -128,14 +118,12 @@ async function bootstrap() {
|
|
|
128
118
|
|
|
129
119
|
await framework.loadControllers()
|
|
130
120
|
|
|
131
|
-
// 错误日志中间件:挂在所有路由之后,记录业务错误并继续传递给默认错误处理。
|
|
132
121
|
app.use(expressErrorHandler())
|
|
133
122
|
|
|
134
123
|
framework.start(Number(process.env.PORT) || 3000)
|
|
135
124
|
|
|
136
125
|
logger.info('[app] Initialization complete (web process)')
|
|
137
126
|
|
|
138
|
-
// 未捕获异常:打印堆栈后直接退出。OS / 进程管理器负责回收资源。
|
|
139
127
|
process.on('uncaughtException', (err) => {
|
|
140
128
|
logger.error(`[app] uncaughtException: ${err.stack || err.message}`)
|
|
141
129
|
process.exit(1)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
const { WebFramework } = require('skweb-framework')
|
|
3
|
+
|
|
4
|
+
const framework = new WebFramework({
|
|
5
|
+
db: {
|
|
6
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
7
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
8
|
+
username: process.env.DB_USER || 'root',
|
|
9
|
+
password: process.env.DB_PASS || '',
|
|
10
|
+
database: process.env.DB_NAME || 'test-project',
|
|
11
|
+
dialect: process.env.DB_DIALECT || 'mysql',
|
|
12
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
13
|
+
underscored: true,
|
|
14
|
+
logging: process.env.DEBUG_DB === 'true'
|
|
15
|
+
},
|
|
16
|
+
redis: {
|
|
17
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
18
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
19
|
+
password: process.env.REDIS_PASSWORD || undefined
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
module.exports = { framework }
|
|
@@ -38,29 +38,25 @@ REDIS_DB=0
|
|
|
38
38
|
# (Production is always disabled to avoid exposing internal API details.)
|
|
39
39
|
SWAGGER_ENABLED=true
|
|
40
40
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# 'jwks': verify via JWKS endpoint (Keycloak / Auth0 / generic OIDC)
|
|
44
|
-
AUTH_PROVIDER={{auth}}
|
|
45
|
-
|
|
46
|
-
# ---------- JWT (static mode) ----------
|
|
41
|
+
{{#if_eq auth "static"}}
|
|
42
|
+
# ---------- Auth: static (HS256 shared secret) ----------
|
|
47
43
|
# [REQUIRED in production] Use a strong random string, e.g. `openssl rand -hex 32`
|
|
48
44
|
JWT_SECRET=please-change-this-in-production
|
|
49
45
|
JWT_EXPIRES_IN=7d
|
|
50
|
-
|
|
51
|
-
# ----------
|
|
46
|
+
{{else}}
|
|
47
|
+
# ---------- Auth: jwks (OIDC compatible) ----------
|
|
52
48
|
# Works with any OIDC-compliant IdP. Common issuer formats:
|
|
53
49
|
# Keycloak: https://kc.example.com/realms/myrealm
|
|
54
50
|
# Auth0: https://your-tenant.auth0.com/
|
|
55
51
|
# Okta: https://your-domain.okta.com/oauth2/default
|
|
56
52
|
# Authing: https://your-app.authing.cn/oauth2/default
|
|
57
|
-
# Keycloak Realm: https://kc.example.com/auth/realms/myrealm
|
|
58
53
|
# `audience` is the client id of this API in the IdP (recommended).
|
|
59
54
|
# JWKS URI is auto-derived as ${OIDC_ISSUER}/.well-known/jwks.json
|
|
60
55
|
# unless OIDC_JWKS_URI is set explicitly.
|
|
61
56
|
OIDC_ISSUER=
|
|
62
57
|
OIDC_AUDIENCE=
|
|
63
58
|
OIDC_JWKS_URI=
|
|
59
|
+
{{/if_eq}}
|
|
64
60
|
|
|
65
61
|
# ---------- Logging ----------
|
|
66
62
|
# error | warn | info | http | verbose | debug | silly
|
|
@@ -2,7 +2,7 @@ module.exports = {
|
|
|
2
2
|
apps: [
|
|
3
3
|
{
|
|
4
4
|
name: '{{name}}-web',
|
|
5
|
-
script: 'src/
|
|
5
|
+
script: 'src/web-app.js',
|
|
6
6
|
instances: 'max',
|
|
7
7
|
exec_mode: 'cluster',
|
|
8
8
|
env: {
|
|
@@ -16,7 +16,7 @@ module.exports = {
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: '{{name}}-cron',
|
|
19
|
-
script: 'src/
|
|
19
|
+
script: 'src/cron-app.js',
|
|
20
20
|
instances: 1,
|
|
21
21
|
exec_mode: 'fork',
|
|
22
22
|
autorestart: true,
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"restart": "pm2 restart all",
|
|
10
10
|
"delete": "pm2 delete all",
|
|
11
11
|
"logs": "pm2 logs",
|
|
12
|
-
"web:dev": "npx dotenvx run -- node --watch src/
|
|
13
|
-
"cron:dev": "npx dotenvx run -- node --watch src/
|
|
12
|
+
"web:dev": "npx dotenvx run -- node --watch src/web-app.js",
|
|
13
|
+
"cron:dev": "npx dotenvx run -- node --watch src/cron-app.js",
|
|
14
14
|
"seed": "npx dotenvx run -- node src/commands/seed.js",
|
|
15
15
|
"migrate": "npx dotenvx run -- node src/commands/migrate.js",
|
|
16
16
|
"build": "cp -r src dist",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@dotenvx/dotenvx": "^1.75.1",
|
|
24
24
|
"http-errors": "^2.0.1",
|
|
25
|
-
"skweb-express": "^2.0.
|
|
25
|
+
"skweb-express": "^2.0.4",
|
|
26
26
|
"skweb-express-jwt": "^3.0.0",
|
|
27
27
|
"skweb-express-logger": "^2.0.0",
|
|
28
28
|
"skweb-sequelize": "^2.0.0",
|
|
29
29
|
"skweb-redis": "^2.0.0",
|
|
30
30
|
"skweb-cron": "^2.0.0",
|
|
31
|
-
"skweb-framework": "^2.1.
|
|
32
|
-
"skweb-plugin": "^2.1.
|
|
31
|
+
"skweb-framework": "^2.1.1",
|
|
32
|
+
"skweb-plugin": "^2.1.1",
|
|
33
33
|
"skweb-util": "^2.0.0",
|
|
34
34
|
"winston": "^3.19.0",
|
|
35
35
|
"sequelize": "^6.37.8",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { framework } from './cron-framework.js'
|
|
6
|
+
import logger from './utils/logger.js'
|
|
7
|
+
import { checkRequiredEnv } from './utils/config.js'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
async function startCronWorker() {
|
|
13
|
+
logger.info('[cron-worker] Starting cron worker process...')
|
|
14
|
+
|
|
15
|
+
if (!checkRequiredEnv()) {
|
|
16
|
+
logger.error('[cron-worker] Configuration validation failed, exiting...')
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await framework.setup({
|
|
21
|
+
modelsDir: path.join(__dirname, './models'),
|
|
22
|
+
cronActionsDir: path.join(__dirname, './cronActions')
|
|
23
|
+
})
|
|
24
|
+
await framework.start()
|
|
25
|
+
logger.info('[cron-worker] Cron worker started')
|
|
26
|
+
|
|
27
|
+
process.on('uncaughtException', (err) => {
|
|
28
|
+
logger.error(`[cron-worker] uncaughtException: ${err.stack || err.message}`)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
})
|
|
31
|
+
process.on('unhandledRejection', (reason) => {
|
|
32
|
+
logger.error(`[cron-worker] unhandledRejection: ${reason instanceof Error ? reason.stack : String(reason)}`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`
|
|
38
|
+
if (isMainModule) {
|
|
39
|
+
startCronWorker().catch((err) => {
|
|
40
|
+
logger.error('[cron-worker] Fatal error:', err)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { startCronWorker }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { CronFramework } from 'skweb-framework'
|
|
3
|
+
import logger from './utils/logger.js'
|
|
4
|
+
|
|
5
|
+
const framework = new CronFramework({
|
|
6
|
+
db: {
|
|
7
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
8
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
9
|
+
username: process.env.DB_USER || 'root',
|
|
10
|
+
password: process.env.DB_PASS || '',
|
|
11
|
+
database: process.env.DB_NAME || 'test-project',
|
|
12
|
+
dialect: process.env.DB_DIALECT || 'mysql',
|
|
13
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
14
|
+
underscored: true,
|
|
15
|
+
logging: process.env.DEBUG_DB === 'true'
|
|
16
|
+
},
|
|
17
|
+
redis: {
|
|
18
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
19
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
20
|
+
password: process.env.REDIS_PASSWORD || undefined
|
|
21
|
+
},
|
|
22
|
+
channel: 'cron:task:cmd',
|
|
23
|
+
logger,
|
|
24
|
+
onSaveTask: async (taskData) => {
|
|
25
|
+
const SysCronTask = framework.db?.sequelize?.models.SysCronTask
|
|
26
|
+
if (SysCronTask) {
|
|
27
|
+
await SysCronTask.update(
|
|
28
|
+
{ cache: taskData.cache, data: taskData.data },
|
|
29
|
+
{ where: { id: taskData.taskId } }
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
loadTasks: async () => {
|
|
34
|
+
const SysCronTask = framework.db?.sequelize?.models.SysCronTask
|
|
35
|
+
if (!SysCronTask) return []
|
|
36
|
+
return (await SysCronTask.findAll()).map((t) => ({
|
|
37
|
+
id: String(t.id),
|
|
38
|
+
cronTime: t.cronTime,
|
|
39
|
+
method: t.method
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export { framework }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
import path from 'path'
|
|
2
3
|
import { fileURLToPath } from 'url'
|
|
3
4
|
import swaggerJSDoc from 'swagger-jsdoc'
|
|
@@ -5,22 +6,6 @@ import swaggerJSDoc from 'swagger-jsdoc'
|
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url)
|
|
6
7
|
const __dirname = path.dirname(__filename)
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
-
* 构建 OpenAPI 3.0 规范对象。
|
|
10
|
-
*
|
|
11
|
-
* 控制器中应使用 @swagger JSDoc 注释描述端点,例如:
|
|
12
|
-
*
|
|
13
|
-
* /**
|
|
14
|
-
* * @swagger
|
|
15
|
-
* * /api/users:
|
|
16
|
-
* * get:
|
|
17
|
-
* * summary: 获取用户列表
|
|
18
|
-
* * tags: [Users]
|
|
19
|
-
* * responses:
|
|
20
|
-
* * 200:
|
|
21
|
-
* * description: OK
|
|
22
|
-
* *\/
|
|
23
|
-
*/
|
|
24
9
|
export function buildOpenApiSpec() {
|
|
25
10
|
return swaggerJSDoc({
|
|
26
11
|
definition: {
|
|
@@ -36,12 +21,22 @@ export function buildOpenApiSpec() {
|
|
|
36
21
|
],
|
|
37
22
|
components: {
|
|
38
23
|
securitySchemes: {
|
|
24
|
+
{{#if_eq auth "static"}}
|
|
39
25
|
bearerAuth: {
|
|
40
26
|
type: 'http',
|
|
41
27
|
scheme: 'bearer',
|
|
42
|
-
bearerFormat: 'JWT'
|
|
43
|
-
description: 'Paste a JWT token here (only needed when AUTH_PROVIDER=static)'
|
|
28
|
+
bearerFormat: 'JWT'
|
|
44
29
|
}
|
|
30
|
+
{{else}}
|
|
31
|
+
oidcAuth: {
|
|
32
|
+
type: 'openIdConnect',
|
|
33
|
+
openIdConnectUrl: process.env.OIDC_JWKS_URI
|
|
34
|
+
? process.env.OIDC_JWKS_URI.replace(/\/jwks\.json$/, '/.well-known/openid-configuration')
|
|
35
|
+
: (process.env.OIDC_ISSUER
|
|
36
|
+
? `${process.env.OIDC_ISSUER.replace(/\/$/, '')}/.well-known/openid-configuration`
|
|
37
|
+
: undefined)
|
|
38
|
+
}
|
|
39
|
+
{{/if_eq}}
|
|
45
40
|
}
|
|
46
41
|
}
|
|
47
42
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
import logger from './logger.js'
|
|
2
3
|
|
|
3
4
|
export function validateConfig() {
|
|
@@ -13,10 +14,6 @@ export function validateConfig() {
|
|
|
13
14
|
logger.warn('[config] DB_USER not set, using default: root')
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
if (process.env.JWT_SECRET === 'please-change-this-in-production') {
|
|
17
|
-
logger.warn('[config] WARNING: JWT_SECRET is using default value, please change in production!')
|
|
18
|
-
}
|
|
19
|
-
|
|
20
17
|
if (!process.env.REDIS_HOST) {
|
|
21
18
|
logger.warn('[config] REDIS_HOST not set, using default: 127.0.0.1')
|
|
22
19
|
}
|
|
@@ -42,22 +39,41 @@ export function validateConfig() {
|
|
|
42
39
|
}
|
|
43
40
|
}
|
|
44
41
|
|
|
42
|
+
{{#if_eq auth "static"}}
|
|
43
|
+
export function checkJwtSecret() {
|
|
44
|
+
if (process.env.JWT_SECRET === 'please-change-this-in-production' || !process.env.JWT_SECRET) {
|
|
45
|
+
if (process.env.NODE_ENV === 'production') {
|
|
46
|
+
logger.error('[config] JWT_SECRET must be set in production!')
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
logger.warn('[config] WARNING: JWT_SECRET is using default value, please change in production!')
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
{{else}}
|
|
54
|
+
export function checkOIDCConfig() {
|
|
55
|
+
const issuer = process.env.OIDC_ISSUER
|
|
56
|
+
const jwksUri = process.env.OIDC_JWKS_URI
|
|
57
|
+
if (!issuer && !jwksUri) {
|
|
58
|
+
logger.error('[config] OIDC_ISSUER or OIDC_JWKS_URI must be set for jwks auth')
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
if (process.env.NODE_ENV === 'production' && !process.env.OIDC_AUDIENCE) {
|
|
62
|
+
logger.warn('[config] OIDC_AUDIENCE is not set; audience verification will be skipped')
|
|
63
|
+
}
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
{{/if_eq}}
|
|
67
|
+
|
|
45
68
|
export function checkRequiredEnv() {
|
|
46
69
|
const result = validateConfig()
|
|
47
|
-
|
|
70
|
+
|
|
48
71
|
if (result.errors.length > 0) {
|
|
49
72
|
logger.error('[config] Configuration validation failed:')
|
|
50
73
|
result.errors.forEach((err) => logger.error(` - ${err}`))
|
|
51
74
|
return false
|
|
52
75
|
}
|
|
53
76
|
|
|
54
|
-
if (process.env.NODE_ENV === 'production') {
|
|
55
|
-
if (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'please-change-this-in-production') {
|
|
56
|
-
logger.error('[config] JWT_SECRET must be set in production!')
|
|
57
|
-
return false
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
77
|
logger.info('[config] Configuration validation passed')
|
|
62
78
|
return true
|
|
63
|
-
}
|
|
79
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
import 'dotenv/config'
|
|
2
3
|
|
|
3
4
|
import path from 'path'
|
|
@@ -5,37 +6,19 @@ import { fileURLToPath } from 'url'
|
|
|
5
6
|
import express from 'skweb-express'
|
|
6
7
|
import expressLogger, { errorHandler as expressErrorHandler } from 'skweb-express-logger'
|
|
7
8
|
import swaggerUi from 'swagger-ui-express'
|
|
9
|
+
import { framework } from './web-framework.js'
|
|
8
10
|
import logger from './utils/logger.js'
|
|
9
|
-
import { checkRequiredEnv } from './utils/config.js'
|
|
11
|
+
import { checkRequiredEnv{{#if_eq auth "static"}}, checkJwtSecret{{/if_eq}}{{#if_eq auth "jwks"}}, checkOIDCConfig{{/if_eq}} } from './utils/config.js'
|
|
10
12
|
import { buildOpenApiSpec } from './swagger.js'
|
|
11
13
|
|
|
12
14
|
const __filename = fileURLToPath(import.meta.url)
|
|
13
15
|
const __dirname = path.dirname(__filename)
|
|
14
16
|
|
|
17
|
+
{{#if_eq auth "static"}}
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
17
|
-
* - 'static' (默认):使用共享密钥(HS256/HS384/HS512),适用于自签 token
|
|
18
|
-
* - 'jwks':通过 JWKS 端点动态获取公钥(RS256 等),适用于 Keycloak/Auth0 等 IdP
|
|
19
|
+
* JWT 配置(static 模式,HS256 共享密钥)。
|
|
19
20
|
*/
|
|
20
21
|
function buildJwtOptions() {
|
|
21
|
-
const provider = process.env.AUTH_PROVIDER || 'static'
|
|
22
|
-
|
|
23
|
-
if (provider === 'jwks') {
|
|
24
|
-
const issuer = process.env.OIDC_ISSUER
|
|
25
|
-
const jwksUri = process.env.OIDC_JWKS_URI
|
|
26
|
-
const audience = process.env.OIDC_AUDIENCE
|
|
27
|
-
if (!issuer && !jwksUri) {
|
|
28
|
-
throw new Error('AUTH_PROVIDER=jwks requires OIDC_ISSUER or OIDC_JWKS_URI')
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
provider: 'jwks',
|
|
32
|
-
algorithms: ['RS256'],
|
|
33
|
-
jwks: { jwksUri, issuer, audience },
|
|
34
|
-
verify: { issuer, audience },
|
|
35
|
-
requestProperty: 'resolvedToken'
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
22
|
return {
|
|
40
23
|
provider: 'static',
|
|
41
24
|
secret: process.env.JWT_SECRET || 'please-change-this-in-production',
|
|
@@ -43,12 +26,27 @@ function buildJwtOptions() {
|
|
|
43
26
|
sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
|
|
44
27
|
requestProperty: 'resolvedToken'
|
|
45
28
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
29
|
+
}{{else}}
|
|
48
30
|
/**
|
|
49
|
-
*
|
|
50
|
-
* 生产环境(production / unset)默认禁用,避免暴露内部 API 细节。
|
|
31
|
+
* JWT 配置(jwks 模式,通过 OIDC 兼容的 JWKS 端点验证 RS256 签名)。
|
|
51
32
|
*/
|
|
33
|
+
function buildJwtOptions() {
|
|
34
|
+
const issuer = process.env.OIDC_ISSUER
|
|
35
|
+
const jwksUri = process.env.OIDC_JWKS_URI
|
|
36
|
+
const audience = process.env.OIDC_AUDIENCE
|
|
37
|
+
if (!issuer && !jwksUri) {
|
|
38
|
+
throw new Error('jwks mode requires OIDC_ISSUER or OIDC_JWKS_URI')
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
provider: 'jwks',
|
|
42
|
+
algorithms: ['RS256'],
|
|
43
|
+
jwks: { jwksUri, issuer, audience },
|
|
44
|
+
verify: { issuer, audience },
|
|
45
|
+
requestProperty: 'resolvedToken'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
{{/if_eq}}
|
|
49
|
+
|
|
52
50
|
function shouldEnableSwagger() {
|
|
53
51
|
const env = (process.env.NODE_ENV || '').toLowerCase()
|
|
54
52
|
if (env !== 'development' && env !== 'test') return false
|
|
@@ -63,27 +61,17 @@ async function bootstrap() {
|
|
|
63
61
|
logger.error('[app] Configuration validation failed, exiting...')
|
|
64
62
|
process.exit(1)
|
|
65
63
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
78
|
-
underscored: true,
|
|
79
|
-
logging: process.env.DEBUG_DB === 'true' ? (msg) => logger.debug(msg) : false
|
|
80
|
-
},
|
|
81
|
-
redis: {
|
|
82
|
-
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
83
|
-
port: Number(process.env.REDIS_PORT) || 6379,
|
|
84
|
-
password: process.env.REDIS_PASSWORD || undefined
|
|
85
|
-
}
|
|
86
|
-
})
|
|
64
|
+
{{#if_eq auth "static"}}
|
|
65
|
+
if (!checkJwtSecret()) {
|
|
66
|
+
logger.error('[app] JWT configuration validation failed, exiting...')
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
{{else}}
|
|
70
|
+
if (!checkOIDCConfig()) {
|
|
71
|
+
logger.error('[app] OIDC configuration validation failed, exiting...')
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
{{/if_eq}}
|
|
87
75
|
|
|
88
76
|
const app = framework.createApp()
|
|
89
77
|
|
|
@@ -101,21 +89,19 @@ async function bootstrap() {
|
|
|
101
89
|
app.setOkCode(0)
|
|
102
90
|
app.setOkMessage('success')
|
|
103
91
|
|
|
104
|
-
// JWT 配置(按 AUTH_PROVIDER 选择 static 或 jwks 模式)
|
|
105
92
|
const jwtOptions = buildJwtOptions()
|
|
106
93
|
app.setJWT('default', jwtOptions)
|
|
107
94
|
app.setJwtHintHeader('JwtHint')
|
|
108
95
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
}
|
|
96
|
+
{{#if_eq auth "static"}}
|
|
97
|
+
logger.info('[app] JWT auth provider: static (HS256)')
|
|
98
|
+
{{else}}
|
|
99
|
+
logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks?.issuer || jwtOptions.jwks?.jwksUri})`)
|
|
100
|
+
{{/if_eq}}
|
|
114
101
|
|
|
115
102
|
app.use(expressLogger())
|
|
116
103
|
app.use(express.static(path.join(__dirname, '..', 'public')))
|
|
117
104
|
|
|
118
|
-
// 仅在 dev / test 环境挂载 Swagger UI(默认关闭,需显式 SWAGGER_ENABLED=true)
|
|
119
105
|
if (shouldEnableSwagger()) {
|
|
120
106
|
const openapiSpec = buildOpenApiSpec()
|
|
121
107
|
app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
|
|
@@ -130,14 +116,12 @@ async function bootstrap() {
|
|
|
130
116
|
|
|
131
117
|
await framework.loadControllers()
|
|
132
118
|
|
|
133
|
-
// 错误日志中间件:挂在所有路由之后,记录业务错误并继续传递给默认错误处理。
|
|
134
119
|
app.use(expressErrorHandler())
|
|
135
120
|
|
|
136
121
|
framework.start(Number(process.env.PORT) || 3000)
|
|
137
122
|
|
|
138
123
|
logger.info('[app] Initialization complete (web process)')
|
|
139
124
|
|
|
140
|
-
// 未捕获异常:打印堆栈后直接退出。OS / 进程管理器负责回收资源。
|
|
141
125
|
process.on('uncaughtException', (err) => {
|
|
142
126
|
logger.error(`[app] uncaughtException: ${err.stack || err.message}`)
|
|
143
127
|
process.exit(1)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { WebFramework } from 'skweb-framework'
|
|
3
|
+
|
|
4
|
+
const framework = new WebFramework({
|
|
5
|
+
db: {
|
|
6
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
7
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
8
|
+
username: process.env.DB_USER || 'root',
|
|
9
|
+
password: process.env.DB_PASS || '',
|
|
10
|
+
database: process.env.DB_NAME || 'test-project',
|
|
11
|
+
dialect: process.env.DB_DIALECT || 'mysql',
|
|
12
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
13
|
+
underscored: true,
|
|
14
|
+
logging: process.env.DEBUG_DB === 'true'
|
|
15
|
+
},
|
|
16
|
+
redis: {
|
|
17
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
18
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
19
|
+
password: process.env.REDIS_PASSWORD || undefined
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export { framework }
|