create-skweb 3.1.0 → 3.2.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.
Files changed (38) hide show
  1. package/.rollup.cache/Users/stayknight/projects/skweb-mono/tools/create-skweb/dist/index.js +19 -1
  2. package/.rollup.cache/Users/stayknight/projects/skweb-mono/tools/create-skweb/tsconfig.tsbuildinfo +1 -1
  3. package/.turbo/turbo-build.log +16 -17
  4. package/.turbo/turbo-lint.log +11 -12
  5. package/.turbo/turbo-test.log +66 -1
  6. package/CHANGELOG.md +49 -0
  7. package/dist/cli.js +19 -1
  8. package/dist/index.js +19 -1
  9. package/package.json +1 -1
  10. package/src/index.ts +19 -1
  11. package/templates/cjs/{.env.example → .env.example.auth-jwks} +1 -12
  12. package/templates/cjs/.env.example.auth-static +54 -0
  13. package/templates/cjs/package.json +3 -3
  14. package/templates/cjs/src/app.auth-jwks.js +131 -0
  15. package/templates/cjs/src/{app.js → app.auth-static.js} +7 -34
  16. package/templates/cjs/src/swagger.auth-jwks.js +38 -0
  17. package/templates/cjs/src/{swagger.js → swagger.auth-static.js} +1 -18
  18. package/templates/cjs/src/utils/config.auth-jwks.js +67 -0
  19. package/templates/cjs/src/utils/{config.js → config.auth-static.js} +13 -13
  20. package/templates/mjs/{.env.example → .env.example.auth-jwks} +1 -12
  21. package/templates/mjs/.env.example.auth-static +54 -0
  22. package/templates/mjs/package.json +3 -3
  23. package/templates/mjs/src/app.auth-jwks.js +137 -0
  24. package/templates/mjs/src/{app.js → app.auth-static.js} +7 -35
  25. package/templates/mjs/src/swagger.auth-jwks.js +38 -0
  26. package/templates/mjs/src/{swagger.js → swagger.auth-static.js} +1 -18
  27. package/templates/mjs/src/utils/config.auth-jwks.js +65 -0
  28. package/templates/mjs/src/utils/{config.js → config.auth-static.js} +13 -13
  29. package/templates/ts/{.env.example → .env.example.auth-jwks} +1 -12
  30. package/templates/ts/.env.example.auth-static +54 -0
  31. package/templates/ts/package.json +3 -3
  32. package/templates/ts/src/app.auth-jwks.ts +137 -0
  33. package/templates/ts/src/{app.ts → app.auth-static.ts} +7 -43
  34. package/templates/ts/src/swagger.auth-jwks.ts +39 -0
  35. package/templates/ts/src/{swagger.ts → swagger.auth-static.ts} +1 -19
  36. package/templates/ts/src/utils/config.auth-jwks.ts +70 -0
  37. package/templates/ts/src/utils/{config.ts → config.auth-static.ts} +13 -13
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,69 @@
1
1
 
2
- > create-skweb@1.2.1 test
2
+ > create-skweb@3.2.0 test
3
3
  > mocha test/**/*.mjs
4
4
 
5
+
6
+
7
+ create-skweb
8
+ createProject()
9
+ ✔ should create TypeScript project from template
10
+ ✔ should create MJS project from template
11
+ ✔ should create CJS project from template
12
+ ✔ should reject non-empty destination directory
13
+ ✔ should include winston in all templates
14
+ ✔ should include @dotenvx/dotenvx in all templates
15
+ ✔ should include pm2 ecosystem in all templates
16
+ ✔ should replace {{name}} placeholder in package.json
17
+ printBanner()
18
+
19
+ ╔════════════════════════════════════════════════════════════════════╗
20
+ ║ Create SKWeb Project ║
21
+ ╚════════════════════════════════════════════════════════════════════╝
22
+
23
+ ✔ should not throw
24
+ printSuccess()
25
+
26
+ ╔════════════════════════════════════════════════════════════════════╗
27
+ ║ 项目创建成功! ║
28
+ ╚════════════════════════════════════════════════════════════════════╝
29
+
30
+ 项目名称: my-app
31
+ 项目路径: /tmp/my-app/my-app
32
+
33
+ 接下来可以执行:
34
+
35
+ cd my-app
36
+
37
+ # 复制环境变量模板并按需修改
38
+ cp .env.example .env
39
+
40
+ # 开发模式运行
41
+ npm run start:dev
42
+
43
+ # 生产模式运行 (需要先构建)
44
+ npm run build
45
+ npm run start
46
+
47
+ # 查看日志
48
+ npm run logs
49
+
50
+ # 停止服务
51
+ npm run stop
52
+
53
+ # 重启服务
54
+ npm run restart
55
+
56
+ 项目特性:
57
+ ✅ TypeScript/MJS/CJS 支持
58
+ ✅ @dotenvx/dotenvx 环境变量管理
59
+ ✅ PM2 进程管理
60
+ ✅ SKWeb Express 集成
61
+ ✅ Keycloak/JWKS 鉴权 (可选)
62
+ ✅ ESLint 代码检查
63
+ ✅ Mocha 测试框架
64
+
65
+ ✔ should not throw
66
+
67
+
68
+ 10 passing (179ms)
69
+
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # create-skweb
2
2
 
3
+ ## 3.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c5338b9: feat(templates): honor `--auth` option at generation time, not at runtime
8
+
9
+ Previously `app.js` / `app.ts` always contained both the static and jwks
10
+ JWT branches and decided which to use at runtime via `process.env.AUTH_PROVIDER`.
11
+ That meant the `--auth` CLI option was effectively a hint, not a real
12
+ choice: every generated project shipped both code paths and the
13
+ `AUTH_PROVIDER=...` line in `.env.example`, even projects that explicitly
14
+ chose static or jwks.
15
+
16
+ Now `createProject` reads the `auth` option and writes only the
17
+ matching files:
18
+
19
+ - `app.auth-static.{js,ts}` → `app.{js,ts}` (static only)
20
+ - `app.auth-jwks.{js,ts}` → `app.{js,ts}` (jwks only)
21
+ - `config.auth-static.{js,ts}` → `config.{js,ts}` (exports `checkJwtSecret`)
22
+ - `config.auth-jwks.{js,ts}` → `config.{js,ts}` (exports `checkOIDCConfig`)
23
+ - `swagger.auth-static.{js,ts}` → `swagger.{js,ts}` (HTTP bearer security)
24
+ - `swagger.auth-jwks.{js,ts}` → `swagger.{js,ts}` (OIDC openIdConnect)
25
+ - `.env.example.auth-static` → `.env.example` (JWT_SECRET only)
26
+ - `.env.example.auth-jwks` → `.env.example` (OIDC\_\* only)
27
+
28
+ Generated projects are now leaner: a static-only project contains no
29
+ JWKS code or env vars, and vice versa. The `checkJwtSecret` /
30
+ `checkOIDCConfig` split also keeps the JWT_SECRET default-value
31
+ warning from leaking into the cron worker process.
32
+
33
+ ## 3.1.2
34
+
35
+ ### Patch Changes
36
+
37
+ - 03ebe56: refactor(templates): separate JWT secret validation from generic env check
38
+
39
+ `validateConfig` (in templates/cjs/mjs/ts `src/utils/config.{js,ts}`) no longer
40
+ warns about `JWT_SECRET` default value. The JWT-specific check is moved to a
41
+ new `checkJwtSecret()` helper that is now only invoked from the web bootstrap
42
+ (`src/app.{js,ts}`), not from `cronWorker.js` or CLI commands. This removes the
43
+ spurious "JWT_SECRET is using default value" warning in the cron worker
44
+ process, which never uses JWT.
45
+
46
+ ## 3.1.1
47
+
48
+ ### Patch Changes
49
+
50
+ - update template versions
51
+
3
52
  ## 3.1.0
4
53
 
5
54
  ### Minor Changes
package/dist/cli.js CHANGED
@@ -32,12 +32,30 @@ async function createProject(options) {
32
32
  await fs.mkdirs(path.join(targetDir, fileName));
33
33
  continue;
34
34
  }
35
+ // 处理 auth-后缀:例如 src/app.auth-static.js -> 生成 src/app.js
36
+ // .env.example.auth-static -> 生成 .env.example
37
+ let outputName = fileName;
38
+ const authMatch = fileName.match(/^(.*?)\.auth-(static|jwks)(\.[^.]+)?$/);
39
+ if (authMatch) {
40
+ const [, base, fileAuth, ext] = authMatch;
41
+ if (fileAuth !== auth) {
42
+ // 不匹配用户选择的 auth,跳过
43
+ continue;
44
+ }
45
+ outputName = ext ? `${base}${ext}` : base;
46
+ }
47
+ else {
48
+ // 没有 auth- 后缀的 .env.example:跳过(避免与 auth- 版本冲突)
49
+ if (fileName === '.env.example' || fileName.endsWith('/.env.example')) {
50
+ continue;
51
+ }
52
+ }
35
53
  let content = await fs.readFile(srcPath, 'utf-8');
36
54
  content = content
37
55
  .replace(/\{\{name\}\}/g, name)
38
56
  .replace(/\{\{description\}\}/g, description)
39
57
  .replace(/\{\{auth\}\}/g, auth);
40
- const destPath = path.join(targetDir, fileName);
58
+ const destPath = path.join(targetDir, outputName);
41
59
  await fs.writeFile(destPath, content);
42
60
  }
43
61
  await fs.mkdirs(path.join(targetDir, 'logs'));
package/dist/index.js CHANGED
@@ -27,12 +27,30 @@ async function createProject(options) {
27
27
  await fs.mkdirs(path.join(targetDir, fileName));
28
28
  continue;
29
29
  }
30
+ // 处理 auth-后缀:例如 src/app.auth-static.js -> 生成 src/app.js
31
+ // .env.example.auth-static -> 生成 .env.example
32
+ let outputName = fileName;
33
+ const authMatch = fileName.match(/^(.*?)\.auth-(static|jwks)(\.[^.]+)?$/);
34
+ if (authMatch) {
35
+ const [, base, fileAuth, ext] = authMatch;
36
+ if (fileAuth !== auth) {
37
+ // 不匹配用户选择的 auth,跳过
38
+ continue;
39
+ }
40
+ outputName = ext ? `${base}${ext}` : base;
41
+ }
42
+ else {
43
+ // 没有 auth- 后缀的 .env.example:跳过(避免与 auth- 版本冲突)
44
+ if (fileName === '.env.example' || fileName.endsWith('/.env.example')) {
45
+ continue;
46
+ }
47
+ }
30
48
  let content = await fs.readFile(srcPath, 'utf-8');
31
49
  content = content
32
50
  .replace(/\{\{name\}\}/g, name)
33
51
  .replace(/\{\{description\}\}/g, description)
34
52
  .replace(/\{\{auth\}\}/g, auth);
35
- const destPath = path.join(targetDir, fileName);
53
+ const destPath = path.join(targetDir, outputName);
36
54
  await fs.writeFile(destPath, content);
37
55
  }
38
56
  await fs.mkdirs(path.join(targetDir, 'logs'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-skweb",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Create a new SKWeb project with TypeScript, CJS, or MJS support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -42,6 +42,24 @@ export async function createProject(options: CreateOptions): Promise<void> {
42
42
  continue
43
43
  }
44
44
 
45
+ // 处理 auth-后缀:例如 src/app.auth-static.js -> 生成 src/app.js
46
+ // .env.example.auth-static -> 生成 .env.example
47
+ let outputName = fileName
48
+ const authMatch = fileName.match(/^(.*?)\.auth-(static|jwks)(\.[^.]+)?$/)
49
+ if (authMatch) {
50
+ const [, base, fileAuth, ext] = authMatch
51
+ if (fileAuth !== auth) {
52
+ // 不匹配用户选择的 auth,跳过
53
+ continue
54
+ }
55
+ outputName = ext ? `${base}${ext}` : base
56
+ } else {
57
+ // 没有 auth- 后缀的 .env.example:跳过(避免与 auth- 版本冲突)
58
+ if (fileName === '.env.example' || fileName.endsWith('/.env.example')) {
59
+ continue
60
+ }
61
+ }
62
+
45
63
  let content = await fs.readFile(srcPath, 'utf-8')
46
64
 
47
65
  content = content
@@ -49,7 +67,7 @@ export async function createProject(options: CreateOptions): Promise<void> {
49
67
  .replace(/\{\{description\}\}/g, description)
50
68
  .replace(/\{\{auth\}\}/g, auth)
51
69
 
52
- const destPath = path.join(targetDir, fileName)
70
+ const destPath = path.join(targetDir, outputName)
53
71
  await fs.writeFile(destPath, content)
54
72
  }
55
73
 
@@ -38,23 +38,12 @@ REDIS_DB=0
38
38
  # (Production is always disabled to avoid exposing internal API details.)
39
39
  SWAGGER_ENABLED=true
40
40
 
41
- # ---------- Auth Provider ----------
42
- # 'static' (default): use a shared secret (HS256)
43
- # 'jwks': verify via JWKS endpoint (Keycloak / Auth0 / generic OIDC)
44
- AUTH_PROVIDER={{auth}}
45
-
46
- # ---------- JWT (static mode) ----------
47
- # [REQUIRED in production] Use a strong random string, e.g. `openssl rand -hex 32`
48
- JWT_SECRET=please-change-this-in-production
49
- JWT_EXPIRES_IN=7d
50
-
51
- # ---------- JWT (jwks mode, OIDC compatible) ----------
41
+ # ---------- Auth: jwks (OIDC compatible) ----------
52
42
  # Works with any OIDC-compliant IdP. Common issuer formats:
53
43
  # Keycloak: https://kc.example.com/realms/myrealm
54
44
  # Auth0: https://your-tenant.auth0.com/
55
45
  # Okta: https://your-domain.okta.com/oauth2/default
56
46
  # Authing: https://your-app.authing.cn/oauth2/default
57
- # Keycloak Realm: https://kc.example.com/auth/realms/myrealm
58
47
  # `audience` is the client id of this API in the IdP (recommended).
59
48
  # JWKS URI is auto-derived as ${OIDC_ISSUER}/.well-known/jwks.json
60
49
  # unless OIDC_JWKS_URI is set explicitly.
@@ -0,0 +1,54 @@
1
+ # ============================================
2
+ # SKWeb Environment Variables Template
3
+ # ============================================
4
+ # Copy this file to .env and fill in your values:
5
+ # cp .env.example .env
6
+ #
7
+ # Variables with default values can be omitted.
8
+ # Variables marked [REQUIRED] must be set in production.
9
+
10
+ # ---------- Node Environment ----------
11
+ # Development | Production | Test
12
+ NODE_ENV=development
13
+
14
+ # ---------- Server ----------
15
+ PORT=3000
16
+ HOST=0.0.0.0
17
+
18
+ # ---------- Database (MySQL / MariaDB) ----------
19
+ DB_HOST=127.0.0.1
20
+ DB_PORT=3306
21
+ DB_USER=root
22
+ DB_PASS=
23
+ DB_NAME={{name}}
24
+ DB_DIALECT=mysql
25
+ DB_CHARSET=utf8mb4
26
+ DEBUG_DB=false
27
+
28
+ # ---------- Redis ----------
29
+ REDIS_HOST=127.0.0.1
30
+ REDIS_PORT=6379
31
+ REDIS_PASSWORD=
32
+ REDIS_DB=0
33
+
34
+ # ---------- Swagger / OpenAPI ----------
35
+ # Mounts Swagger UI at /api-docs and exposes raw spec at /api-docs.json.
36
+ # Only enabled when NODE_ENV is 'development' or 'test'.
37
+ # Set SWAGGER_ENABLED=false to force-disable in dev/test as well.
38
+ # (Production is always disabled to avoid exposing internal API details.)
39
+ SWAGGER_ENABLED=true
40
+
41
+ # ---------- Auth: static (HS256 shared secret) ----------
42
+ # [REQUIRED in production] Use a strong random string, e.g. `openssl rand -hex 32`
43
+ JWT_SECRET=please-change-this-in-production
44
+ JWT_EXPIRES_IN=7d
45
+
46
+ # ---------- Logging ----------
47
+ # error | warn | info | http | verbose | debug | silly
48
+ LOG_LEVEL=info
49
+ LOG_DIR=./logs
50
+ LOG_CONSOLE=true
51
+
52
+ # ---------- CORS ----------
53
+ # Comma-separated origins, or * for all
54
+ CORS_ORIGIN=*
@@ -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.0",
25
+ "skweb-express": "^2.0.3",
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.0.0",
32
- "skweb-plugin": "^2.0.0",
31
+ "skweb-framework": "^2.1.0",
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,131 @@
1
+ require('dotenv/config')
2
+
3
+ const path = require('path')
4
+ const express = require('skweb-express').default
5
+ const expressLogger = require('skweb-express-logger').default
6
+ const expressErrorHandler = require('skweb-express-logger').errorHandler
7
+ const swaggerUi = require('swagger-ui-express')
8
+ const { WebFramework } = require('skweb-framework')
9
+ const { logger } = require('./utils/logger.js')
10
+ const { checkRequiredEnv, checkOIDCConfig } = require('./utils/config.js')
11
+ const { buildOpenApiSpec } = require('./swagger.js')
12
+
13
+ /**
14
+ * JWT 配置(jwks 模式,通过 OIDC 兼容的 JWKS 端点验证 RS256 签名)。
15
+ */
16
+ function buildJwtOptions() {
17
+ const issuer = process.env.OIDC_ISSUER
18
+ const jwksUri = process.env.OIDC_JWKS_URI
19
+ const audience = process.env.OIDC_AUDIENCE
20
+ if (!issuer && !jwksUri) {
21
+ throw new Error('jwks mode requires OIDC_ISSUER or OIDC_JWKS_URI')
22
+ }
23
+ return {
24
+ provider: 'jwks',
25
+ algorithms: ['RS256'],
26
+ jwks: { jwksUri, issuer, audience },
27
+ verify: { issuer, audience },
28
+ requestProperty: 'resolvedToken'
29
+ }
30
+ }
31
+
32
+ function shouldEnableSwagger() {
33
+ const env = (process.env.NODE_ENV || '').toLowerCase()
34
+ if (env !== 'development' && env !== 'test') return false
35
+ if ((process.env.SWAGGER_ENABLED || '').toLowerCase() === 'false') return false
36
+ return true
37
+ }
38
+
39
+ async function bootstrap() {
40
+ logger.info('[app] Initializing web process...')
41
+
42
+ if (!checkRequiredEnv()) {
43
+ logger.error('[app] Configuration validation failed, exiting...')
44
+ process.exit(1)
45
+ }
46
+ if (!checkOIDCConfig()) {
47
+ logger.error('[app] OIDC configuration validation failed, exiting...')
48
+ process.exit(1)
49
+ }
50
+
51
+ const framework = new WebFramework({
52
+ db: {
53
+ host: process.env.DB_HOST || '127.0.0.1',
54
+ port: Number(process.env.DB_PORT) || 3306,
55
+ username: process.env.DB_USER || 'root',
56
+ password: process.env.DB_PASS || '',
57
+ database: process.env.DB_NAME || 'test-project',
58
+ dialect: process.env.DB_DIALECT || 'mysql',
59
+ charset: process.env.DB_CHARSET || 'utf8mb4',
60
+ underscored: true,
61
+ logging: process.env.DEBUG_DB === 'true' ? (msg) => logger.debug(msg) : false
62
+ },
63
+ redis: {
64
+ host: process.env.REDIS_HOST || '127.0.0.1',
65
+ port: Number(process.env.REDIS_PORT) || 6379,
66
+ password: process.env.REDIS_PASSWORD || undefined
67
+ }
68
+ })
69
+ const app = framework.createApp()
70
+
71
+ await framework.setup({
72
+ modelsDir: path.join(__dirname, './models'),
73
+ controllersDir: path.join(__dirname, './controllers')
74
+ })
75
+
76
+ app.disable('x-powered-by')
77
+ app.disable('etag')
78
+
79
+ app.set('trust proxy', true)
80
+
81
+ app.set('wrap response', true)
82
+ app.setOkCode(0)
83
+ app.setOkMessage('success')
84
+
85
+ const jwtOptions = buildJwtOptions()
86
+ app.setJWT('default', jwtOptions)
87
+ app.setJwtHintHeader('JwtHint')
88
+
89
+ logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks.issuer || jwtOptions.jwks.jwksUri})`)
90
+
91
+ app.use(expressLogger())
92
+ app.use(express.static(path.join(__dirname, '..', 'public')))
93
+
94
+ if (shouldEnableSwagger()) {
95
+ const openapiSpec = buildOpenApiSpec()
96
+ app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
97
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpec, {
98
+ customSiteTitle: `${process.env.APP_NAME || 'SKWeb'} API Docs`,
99
+ swaggerOptions: { persistAuthorization: true }
100
+ }))
101
+ logger.info(`[app] Swagger UI mounted at http://localhost:${process.env.PORT || 3000}/api-docs (env: ${process.env.NODE_ENV})`)
102
+ } else {
103
+ logger.info(`[app] Swagger UI disabled (env: ${process.env.NODE_ENV || 'unset'})`)
104
+ }
105
+
106
+ await framework.loadControllers()
107
+
108
+ app.use(expressErrorHandler())
109
+
110
+ framework.start(Number(process.env.PORT) || 3000)
111
+
112
+ logger.info('[app] Initialization complete (web process)')
113
+
114
+ process.on('uncaughtException', (err) => {
115
+ logger.error(`[app] uncaughtException: ${err.stack || err.message}`)
116
+ process.exit(1)
117
+ })
118
+ process.on('unhandledRejection', (reason) => {
119
+ logger.error(`[app] unhandledRejection: ${reason instanceof Error ? reason.stack : String(reason)}`)
120
+ process.exit(1)
121
+ })
122
+ }
123
+
124
+ if (require.main === module) {
125
+ bootstrap().catch((err) => {
126
+ logger.error('[app] Fatal error:', err)
127
+ process.exit(1)
128
+ })
129
+ }
130
+
131
+ module.exports = { bootstrap }
@@ -1,42 +1,19 @@
1
1
  require('dotenv/config')
2
2
 
3
3
  const path = require('path')
4
- // skweb-express / skweb-express-logger 的 CJS 产物形态是
5
- // `{ default: fn, errorHandler, ... }`。直接 `require('skweb-express')` 拿到的是
6
- // 命名空间对象,`express()` 会 TypeError。这里走 .default。
7
4
  const express = require('skweb-express').default
8
5
  const expressLogger = require('skweb-express-logger').default
9
6
  const expressErrorHandler = require('skweb-express-logger').errorHandler
10
7
  const swaggerUi = require('swagger-ui-express')
11
8
  const { WebFramework } = require('skweb-framework')
12
9
  const { logger } = require('./utils/logger.js')
13
- const { checkRequiredEnv } = require('./utils/config.js')
10
+ const { checkRequiredEnv, checkJwtSecret } = require('./utils/config.js')
14
11
  const { buildOpenApiSpec } = require('./swagger.js')
15
12
 
16
13
  /**
17
- * 构建 JWT 配置,支持两种模式:
18
- * - 'static' (默认):使用共享密钥(HS256/HS384/HS512),适用于自签 token
19
- * - 'jwks':通过 JWKS 端点动态获取公钥(RS256 等),适用于 Keycloak/Auth0 等 IdP
14
+ * JWT 配置(static 模式,HS256 共享密钥)。
20
15
  */
21
16
  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
17
  return {
41
18
  provider: 'static',
42
19
  secret: process.env.JWT_SECRET || 'please-change-this-in-production',
@@ -64,6 +41,10 @@ async function bootstrap() {
64
41
  logger.error('[app] Configuration validation failed, exiting...')
65
42
  process.exit(1)
66
43
  }
44
+ if (!checkJwtSecret()) {
45
+ logger.error('[app] JWT configuration validation failed, exiting...')
46
+ process.exit(1)
47
+ }
67
48
 
68
49
  const framework = new WebFramework({
69
50
  db: {
@@ -99,21 +80,15 @@ async function bootstrap() {
99
80
  app.setOkCode(0)
100
81
  app.setOkMessage('success')
101
82
 
102
- // JWT 配置(按 AUTH_PROVIDER 选择 static 或 jwks 模式)
103
83
  const jwtOptions = buildJwtOptions()
104
84
  app.setJWT('default', jwtOptions)
105
85
  app.setJwtHintHeader('JwtHint')
106
86
 
107
- if (jwtOptions.provider === 'jwks') {
108
- logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks.issuer || jwtOptions.jwks.jwksUri})`)
109
- } else {
110
- logger.info('[app] JWT auth provider: static (HS256)')
111
- }
87
+ logger.info('[app] JWT auth provider: static (HS256)')
112
88
 
113
89
  app.use(expressLogger())
114
90
  app.use(express.static(path.join(__dirname, '..', 'public')))
115
91
 
116
- // 仅在 dev / test 环境挂载 Swagger UI(默认关闭,需显式 SWAGGER_ENABLED=true)
117
92
  if (shouldEnableSwagger()) {
118
93
  const openapiSpec = buildOpenApiSpec()
119
94
  app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
@@ -128,14 +103,12 @@ async function bootstrap() {
128
103
 
129
104
  await framework.loadControllers()
130
105
 
131
- // 错误日志中间件:挂在所有路由之后,记录业务错误并继续传递给默认错误处理。
132
106
  app.use(expressErrorHandler())
133
107
 
134
108
  framework.start(Number(process.env.PORT) || 3000)
135
109
 
136
110
  logger.info('[app] Initialization complete (web process)')
137
111
 
138
- // 未捕获异常:打印堆栈后直接退出。OS / 进程管理器负责回收资源。
139
112
  process.on('uncaughtException', (err) => {
140
113
  logger.error(`[app] uncaughtException: ${err.stack || err.message}`)
141
114
  process.exit(1)
@@ -0,0 +1,38 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+ const swaggerJSDoc = require('swagger-jsdoc')
5
+
6
+ function buildOpenApiSpec() {
7
+ return swaggerJSDoc({
8
+ definition: {
9
+ openapi: '3.0.3',
10
+ info: {
11
+ title: process.env.APP_NAME || 'SKWeb API',
12
+ version: process.env.API_VERSION || '1.0.0',
13
+ description: process.env.APP_DESCRIPTION || 'SKWeb project API documentation',
14
+ contact: { name: 'API Support' }
15
+ },
16
+ servers: [
17
+ { url: `http://localhost:${process.env.PORT || 3000}`, description: 'Local development' }
18
+ ],
19
+ components: {
20
+ securitySchemes: {
21
+ oidcAuth: {
22
+ type: 'openIdConnect',
23
+ openIdConnectUrl: process.env.OIDC_JWKS_URI
24
+ ? process.env.OIDC_JWKS_URI.replace(/\/jwks\.json$/, '/.well-known/openid-configuration')
25
+ : (process.env.OIDC_ISSUER
26
+ ? `${process.env.OIDC_ISSUER.replace(/\/$/, '')}/.well-known/openid-configuration`
27
+ : undefined)
28
+ }
29
+ }
30
+ }
31
+ },
32
+ apis: [
33
+ path.join(__dirname, 'controllers', '*.js')
34
+ ]
35
+ })
36
+ }
37
+
38
+ module.exports = { buildOpenApiSpec }
@@ -3,22 +3,6 @@
3
3
  const path = require('path')
4
4
  const swaggerJSDoc = require('swagger-jsdoc')
5
5
 
6
- /**
7
- * 构建 OpenAPI 3.0 规范对象。
8
- *
9
- * 控制器中应使用 @swagger JSDoc 注释描述端点,例如:
10
- *
11
- * /**
12
- * * @swagger
13
- * * /api/users:
14
- * * get:
15
- * * summary: 获取用户列表
16
- * * tags: [Users]
17
- * * responses:
18
- * * 200:
19
- * * description: OK
20
- * *\/
21
- */
22
6
  function buildOpenApiSpec() {
23
7
  return swaggerJSDoc({
24
8
  definition: {
@@ -37,8 +21,7 @@ function buildOpenApiSpec() {
37
21
  bearerAuth: {
38
22
  type: 'http',
39
23
  scheme: 'bearer',
40
- bearerFormat: 'JWT',
41
- description: 'Paste a JWT token here (only needed when AUTH_PROVIDER=static)'
24
+ bearerFormat: 'JWT'
42
25
  }
43
26
  }
44
27
  }