create-skweb 1.1.4 → 2.0.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.
@@ -4,12 +4,58 @@ import path from 'path'
4
4
  import { fileURLToPath } from 'url'
5
5
  import express from 'skweb-express'
6
6
  import expressLogger from 'skweb-express-logger'
7
+ import swaggerUi from 'swagger-ui-express'
7
8
  import logger from './utils/logger.js'
8
9
  import { checkRequiredEnv } from './utils/config.js'
10
+ import { buildOpenApiSpec } from './swagger.js'
9
11
 
10
12
  const __filename = fileURLToPath(import.meta.url)
11
13
  const __dirname = path.dirname(__filename)
12
14
 
15
+ /**
16
+ * 构建 JWT 配置,支持两种模式:
17
+ * - 'static' (默认):使用共享密钥(HS256/HS384/HS512),适用于自签 token
18
+ * - 'jwks':通过 JWKS 端点动态获取公钥(RS256 等),适用于 Keycloak/Auth0 等 IdP
19
+ */
20
+ 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
+ return {
40
+ provider: 'static',
41
+ secret: process.env.JWT_SECRET || 'please-change-this-in-production',
42
+ algorithms: ['HS256'],
43
+ sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
44
+ requestProperty: 'resolvedToken'
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Swagger UI 仅在 NODE_ENV 为 development / test,且 SWAGGER_ENABLED !== 'false' 时启用。
50
+ * 生产环境(production / unset)默认禁用,避免暴露内部 API 细节。
51
+ */
52
+ function shouldEnableSwagger() {
53
+ const env = (process.env.NODE_ENV || '').toLowerCase()
54
+ if (env !== 'development' && env !== 'test') return false
55
+ if ((process.env.SWAGGER_ENABLED || '').toLowerCase() === 'false') return false
56
+ return true
57
+ }
58
+
13
59
  async function bootstrap() {
14
60
  logger.info('[app] Initializing web process...')
15
61
 
@@ -55,17 +101,33 @@ async function bootstrap() {
55
101
  app.setOkCode(0)
56
102
  app.setOkMessage('success')
57
103
 
58
- app.setJWT('default', {
59
- secret: process.env.JWT_SECRET || 'please-change-this-in-production',
60
- algorithms: ['HS256'],
61
- sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
62
- requestProperty: 'resolvedToken'
63
- })
104
+ // JWT 配置(按 AUTH_PROVIDER 选择 static 或 jwks 模式)
105
+ const jwtOptions = buildJwtOptions()
106
+ app.setJWT('default', jwtOptions)
64
107
  app.setJwtHintHeader('JwtHint')
65
108
 
109
+ if (jwtOptions.provider === 'jwks') {
110
+ logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks.issuer || jwtOptions.jwks.jwksUri})`)
111
+ } else {
112
+ logger.info('[app] JWT auth provider: static (HS256)')
113
+ }
114
+
66
115
  app.use(expressLogger())
67
116
  app.use(express.static(path.join(__dirname, '..', 'public')))
68
117
 
118
+ // 仅在 dev / test 环境挂载 Swagger UI(默认关闭,需显式 SWAGGER_ENABLED=true)
119
+ if (shouldEnableSwagger()) {
120
+ const openapiSpec = buildOpenApiSpec()
121
+ app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
122
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpec, {
123
+ customSiteTitle: `${process.env.APP_NAME || 'SKWeb'} API Docs`,
124
+ swaggerOptions: { persistAuthorization: true }
125
+ }))
126
+ logger.info(`[app] Swagger UI mounted at http://localhost:${process.env.PORT || 3000}/api-docs (env: ${process.env.NODE_ENV})`)
127
+ } else {
128
+ logger.info(`[app] Swagger UI disabled (env: ${process.env.NODE_ENV || 'unset'})`)
129
+ }
130
+
69
131
  await framework.loadControllers(path.join(__dirname, './controllers'))
70
132
  await framework.loadPluginControllers()
71
133
 
@@ -94,4 +156,4 @@ if (isMainModule) {
94
156
  })
95
157
  }
96
158
 
97
- export { bootstrap }
159
+ export { bootstrap }
@@ -1,3 +1,49 @@
1
+ /**
2
+ * @swagger
3
+ * /:
4
+ * get:
5
+ * summary: 欢迎页
6
+ * description: 返回项目基本信息。
7
+ * tags: [Hello]
8
+ * responses:
9
+ * 200:
10
+ * description: OK
11
+ * content:
12
+ * application/json:
13
+ * schema:
14
+ * type: object
15
+ * properties:
16
+ * message: { type: string, example: 'Welcome to SKWeb!' }
17
+ * version: { type: string, example: '1.0.0' }
18
+ * timestamp: { type: string, format: date-time }
19
+ * /health:
20
+ * get:
21
+ * summary: 健康检查
22
+ * tags: [Hello]
23
+ * responses:
24
+ * 200:
25
+ * description: OK
26
+ * content:
27
+ * application/json:
28
+ * schema:
29
+ * type: object
30
+ * properties:
31
+ * status: { type: string, example: 'ok' }
32
+ * timestamp: { type: string, format: date-time }
33
+ * /echo:
34
+ * post:
35
+ * summary: 回显请求体
36
+ * tags: [Hello]
37
+ * requestBody:
38
+ * required: true
39
+ * content:
40
+ * application/json:
41
+ * schema:
42
+ * type: object
43
+ * responses:
44
+ * 200:
45
+ * description: OK
46
+ */
1
47
  export default [
2
48
  {
3
49
  method: 'get',
@@ -0,0 +1,52 @@
1
+ import path from 'path'
2
+ import { fileURLToPath } from 'url'
3
+ import swaggerJSDoc from 'swagger-jsdoc'
4
+
5
+ const __filename = fileURLToPath(import.meta.url)
6
+ const __dirname = path.dirname(__filename)
7
+
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
+ export function buildOpenApiSpec() {
25
+ return swaggerJSDoc({
26
+ definition: {
27
+ openapi: '3.0.3',
28
+ info: {
29
+ title: process.env.APP_NAME || 'SKWeb API',
30
+ version: process.env.API_VERSION || '1.0.0',
31
+ description: process.env.APP_DESCRIPTION || 'SKWeb project API documentation',
32
+ contact: { name: 'API Support' }
33
+ },
34
+ servers: [
35
+ { url: `http://localhost:${process.env.PORT || 3000}`, description: 'Local development' }
36
+ ],
37
+ components: {
38
+ securitySchemes: {
39
+ bearerAuth: {
40
+ type: 'http',
41
+ scheme: 'bearer',
42
+ bearerFormat: 'JWT',
43
+ description: 'Paste a JWT token here (only needed when AUTH_PROVIDER=static)'
44
+ }
45
+ }
46
+ }
47
+ },
48
+ apis: [
49
+ path.join(__dirname, 'controllers', '*.js')
50
+ ]
51
+ })
52
+ }
@@ -0,0 +1,73 @@
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 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) ----------
52
+ # Works with any OIDC-compliant IdP. Common issuer formats:
53
+ # Keycloak: https://kc.example.com/realms/myrealm
54
+ # Auth0: https://your-tenant.auth0.com/
55
+ # Okta: https://your-domain.okta.com/oauth2/default
56
+ # Authing: https://your-app.authing.cn/oauth2/default
57
+ # Keycloak Realm: https://kc.example.com/auth/realms/myrealm
58
+ # `audience` is the client id of this API in the IdP (recommended).
59
+ # JWKS URI is auto-derived as ${OIDC_ISSUER}/.well-known/jwks.json
60
+ # unless OIDC_JWKS_URI is set explicitly.
61
+ OIDC_ISSUER=
62
+ OIDC_AUDIENCE=
63
+ OIDC_JWKS_URI=
64
+
65
+ # ---------- Logging ----------
66
+ # error | warn | info | http | verbose | debug | silly
67
+ LOG_LEVEL=info
68
+ LOG_DIR=./logs
69
+ LOG_CONSOLE=true
70
+
71
+ # ---------- CORS ----------
72
+ # Comma-separated origins, or * for all
73
+ CORS_ORIGIN=*
@@ -25,7 +25,7 @@
25
25
  "@dotenvx/dotenvx": "^1.71.0",
26
26
  "http-errors": "^2.0.0",
27
27
  "skweb-express": "^1.0.0",
28
- "skweb-express-jwt": "^1.0.0",
28
+ "skweb-express-jwt": "^1.2.0",
29
29
  "skweb-express-logger": "^1.0.0",
30
30
  "skweb-sequelize": "^1.0.0",
31
31
  "skweb-redis": "^1.0.0",
@@ -34,12 +34,16 @@
34
34
  "skweb-plugin": "^1.0.0",
35
35
  "skweb-util": "^1.0.0",
36
36
  "winston": "^3.17.0",
37
- "sequelize": "^6.37.7"
37
+ "sequelize": "^6.37.7",
38
+ "swagger-jsdoc": "^6.2.8",
39
+ "swagger-ui-express": "^5.0.1"
38
40
  },
39
41
  "devDependencies": {
40
42
  "@types/node": "^25.0.1",
41
43
  "@types/mocha": "^10.0.10",
42
44
  "@types/http-errors": "^2.0.4",
45
+ "@types/swagger-jsdoc": "^6.0.4",
46
+ "@types/swagger-ui-express": "^4.1.8",
43
47
  "chai": "^6.2.1",
44
48
  "mocha": "^11.7.5",
45
49
  "tsx": "^4.22.4",
@@ -5,12 +5,66 @@ import { fileURLToPath } from 'url'
5
5
  import express from 'skweb-express'
6
6
  import expressLogger from 'skweb-express-logger'
7
7
  import { WebFramework } from 'skweb-framework'
8
+ import type { JWTOptions } from 'skweb-express-jwt'
9
+ import swaggerUi from 'swagger-ui-express'
8
10
  import logger from './utils/logger.js'
9
11
  import { checkRequiredEnv } from './utils/config.js'
12
+ import { buildOpenApiSpec } from './swagger.js'
10
13
 
11
14
  const __filename = fileURLToPath(import.meta.url)
12
15
  const __dirname = path.dirname(__filename)
13
16
 
17
+ /**
18
+ * 构建 JWT 配置,支持两种模式:
19
+ * - 'static' (默认):使用共享密钥(HS256/HS384/HS512),适用于自签 token
20
+ * - 'jwks':通过 JWKS 端点动态获取公钥(RS256 等),适用于 Keycloak/Auth0 等 IdP
21
+ */
22
+ function buildJwtOptions(): JWTOptions {
23
+ const provider = (process.env.AUTH_PROVIDER || 'static') as 'static' | 'jwks'
24
+
25
+ if (provider === 'jwks') {
26
+ const issuer = process.env.OIDC_ISSUER
27
+ const jwksUri = process.env.OIDC_JWKS_URI
28
+ const audience = process.env.OIDC_AUDIENCE
29
+ if (!issuer && !jwksUri) {
30
+ throw new Error('AUTH_PROVIDER=jwks requires OIDC_ISSUER or OIDC_JWKS_URI')
31
+ }
32
+ return {
33
+ provider: 'jwks',
34
+ algorithms: ['RS256'],
35
+ jwks: {
36
+ jwksUri,
37
+ issuer,
38
+ audience
39
+ },
40
+ verify: {
41
+ issuer,
42
+ audience
43
+ },
44
+ requestProperty: 'resolvedToken'
45
+ }
46
+ }
47
+
48
+ return {
49
+ provider: 'static',
50
+ secret: process.env.JWT_SECRET || 'please-change-this-in-production',
51
+ algorithms: ['HS256'],
52
+ sign: { expiresIn: (process.env.JWT_EXPIRES_IN || '7d') as any },
53
+ requestProperty: 'resolvedToken'
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Swagger UI 仅在 NODE_ENV 为 development / test,且 SWAGGER_ENABLED !== 'false' 时启用。
59
+ * 生产环境(production / unset)默认禁用,避免暴露内部 API 细节。
60
+ */
61
+ function shouldEnableSwagger(): boolean {
62
+ const env = (process.env.NODE_ENV || '').toLowerCase()
63
+ if (env !== 'development' && env !== 'test') return false
64
+ if ((process.env.SWAGGER_ENABLED || '').toLowerCase() === 'false') return false
65
+ return true
66
+ }
67
+
14
68
  async function bootstrap(): Promise<void> {
15
69
  logger.info('[app] Initializing web process...')
16
70
 
@@ -54,17 +108,33 @@ async function bootstrap(): Promise<void> {
54
108
  app.setOkCode(0)
55
109
  app.setOkMessage('success')
56
110
 
57
- app.setJWT('default', {
58
- secret: process.env.JWT_SECRET || 'please-change-this-in-production',
59
- algorithms: ['HS256'],
60
- sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
61
- requestProperty: 'resolvedToken'
62
- })
111
+ // JWT 配置(按 AUTH_PROVIDER 选择 static 或 jwks 模式)
112
+ const jwtOptions = buildJwtOptions()
113
+ app.setJWT('default', jwtOptions as any)
63
114
  app.setJwtHintHeader('JwtHint')
64
115
 
116
+ if (jwtOptions.provider === 'jwks') {
117
+ logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks.issuer || jwtOptions.jwks.jwksUri})`)
118
+ } else {
119
+ logger.info('[app] JWT auth provider: static (HS256)')
120
+ }
121
+
65
122
  app.use(expressLogger())
66
123
  app.use(express.static(path.join(__dirname, '..', 'public')))
67
124
 
125
+ // 仅在 dev / test 环境挂载 Swagger UI(默认关闭,需显式 SWAGGER_ENABLED=true)
126
+ if (shouldEnableSwagger()) {
127
+ const openapiSpec = buildOpenApiSpec()
128
+ app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
129
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpec, {
130
+ customSiteTitle: `${process.env.APP_NAME || 'SKWeb'} API Docs`,
131
+ swaggerOptions: { persistAuthorization: true }
132
+ }))
133
+ logger.info(`[app] Swagger UI mounted at http://localhost:${process.env.PORT || 3000}/api-docs (env: ${process.env.NODE_ENV})`)
134
+ } else {
135
+ logger.info(`[app] Swagger UI disabled (env: ${process.env.NODE_ENV || 'unset'})`)
136
+ }
137
+
68
138
  await framework.loadControllers(path.join(__dirname, './controllers'))
69
139
  await framework.loadPluginControllers()
70
140
 
@@ -93,4 +163,4 @@ if (isMainModule) {
93
163
  })
94
164
  }
95
165
 
96
- export { bootstrap }
166
+ export { bootstrap }
@@ -1,3 +1,49 @@
1
+ /**
2
+ * @swagger
3
+ * /:
4
+ * get:
5
+ * summary: 欢迎页
6
+ * description: 返回项目基本信息。
7
+ * tags: [Hello]
8
+ * responses:
9
+ * 200:
10
+ * description: OK
11
+ * content:
12
+ * application/json:
13
+ * schema:
14
+ * type: object
15
+ * properties:
16
+ * message: { type: string, example: 'Welcome to SKWeb!' }
17
+ * version: { type: string, example: '1.0.0' }
18
+ * timestamp: { type: string, format: date-time }
19
+ * /health:
20
+ * get:
21
+ * summary: 健康检查
22
+ * tags: [Hello]
23
+ * responses:
24
+ * 200:
25
+ * description: OK
26
+ * content:
27
+ * application/json:
28
+ * schema:
29
+ * type: object
30
+ * properties:
31
+ * status: { type: string, example: 'ok' }
32
+ * timestamp: { type: string, format: date-time }
33
+ * /echo:
34
+ * post:
35
+ * summary: 回显请求体
36
+ * tags: [Hello]
37
+ * requestBody:
38
+ * required: true
39
+ * content:
40
+ * application/json:
41
+ * schema:
42
+ * type: object
43
+ * responses:
44
+ * 200:
45
+ * description: OK
46
+ */
1
47
  export default [
2
48
  {
3
49
  method: 'get',
@@ -0,0 +1,54 @@
1
+ import path from 'path'
2
+ import { fileURLToPath } from 'url'
3
+ import swaggerJSDoc from 'swagger-jsdoc'
4
+
5
+ const __filename = fileURLToPath(import.meta.url)
6
+ const __dirname = path.dirname(__filename)
7
+
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
+ export function buildOpenApiSpec(): object {
25
+ return swaggerJSDoc({
26
+ definition: {
27
+ openapi: '3.0.3',
28
+ info: {
29
+ title: process.env.APP_NAME || 'SKWeb API',
30
+ version: process.env.API_VERSION || '1.0.0',
31
+ description: process.env.APP_DESCRIPTION || 'SKWeb project API documentation',
32
+ contact: { name: 'API Support' }
33
+ },
34
+ servers: [
35
+ { url: `http://localhost:${process.env.PORT || 3000}`, description: 'Local development' }
36
+ ],
37
+ components: {
38
+ securitySchemes: {
39
+ bearerAuth: {
40
+ type: 'http',
41
+ scheme: 'bearer',
42
+ bearerFormat: 'JWT',
43
+ description: 'Paste a JWT token here (only needed when AUTH_PROVIDER=static)'
44
+ }
45
+ }
46
+ }
47
+ },
48
+ // 扫描 controllers 目录与 src 根下的 JSDoc 注释
49
+ apis: [
50
+ path.join(__dirname, 'controllers', '*.ts'),
51
+ path.join(__dirname, 'controllers', '*.js')
52
+ ]
53
+ })
54
+ }