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.
- package/.rollup.cache/Users/stayknight/projects/skweb-mono/tools/create-skweb/dist/index.js +19 -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 +19 -1
- package/dist/index.js +19 -1
- package/package.json +1 -1
- package/src/index.ts +19 -1
- package/templates/cjs/{.env.example → .env.example.auth-jwks} +1 -12
- package/templates/cjs/.env.example.auth-static +54 -0
- package/templates/cjs/package.json +3 -3
- package/templates/cjs/src/app.auth-jwks.js +131 -0
- package/templates/cjs/src/{app.js → app.auth-static.js} +7 -34
- package/templates/cjs/src/swagger.auth-jwks.js +38 -0
- package/templates/cjs/src/{swagger.js → swagger.auth-static.js} +1 -18
- package/templates/cjs/src/utils/config.auth-jwks.js +67 -0
- package/templates/cjs/src/utils/{config.js → config.auth-static.js} +13 -13
- package/templates/mjs/{.env.example → .env.example.auth-jwks} +1 -12
- package/templates/mjs/.env.example.auth-static +54 -0
- package/templates/mjs/package.json +3 -3
- package/templates/mjs/src/app.auth-jwks.js +137 -0
- package/templates/mjs/src/{app.js → app.auth-static.js} +7 -35
- package/templates/mjs/src/swagger.auth-jwks.js +38 -0
- package/templates/mjs/src/{swagger.js → swagger.auth-static.js} +1 -18
- package/templates/mjs/src/utils/config.auth-jwks.js +65 -0
- package/templates/mjs/src/utils/{config.js → config.auth-static.js} +13 -13
- package/templates/ts/{.env.example → .env.example.auth-jwks} +1 -12
- package/templates/ts/.env.example.auth-static +54 -0
- package/templates/ts/package.json +3 -3
- package/templates/ts/src/app.auth-jwks.ts +137 -0
- package/templates/ts/src/{app.ts → app.auth-static.ts} +7 -43
- package/templates/ts/src/swagger.auth-jwks.ts +39 -0
- package/templates/ts/src/{swagger.ts → swagger.auth-static.ts} +1 -19
- package/templates/ts/src/utils/config.auth-jwks.ts +70 -0
- package/templates/ts/src/utils/{config.ts → config.auth-static.ts} +13 -13
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const { logger } = require('./logger.js')
|
|
2
|
+
|
|
3
|
+
function validateConfig() {
|
|
4
|
+
const errors = []
|
|
5
|
+
|
|
6
|
+
if (!process.env.DB_HOST) {
|
|
7
|
+
logger.warn('[config] DB_HOST not set, using default: 127.0.0.1')
|
|
8
|
+
}
|
|
9
|
+
if (!process.env.DB_NAME) {
|
|
10
|
+
logger.warn('[config] DB_NAME not set, using default: test-project')
|
|
11
|
+
}
|
|
12
|
+
if (!process.env.DB_USER) {
|
|
13
|
+
logger.warn('[config] DB_USER not set, using default: root')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!process.env.REDIS_HOST) {
|
|
17
|
+
logger.warn('[config] REDIS_HOST not set, using default: 127.0.0.1')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const port = Number(process.env.PORT)
|
|
21
|
+
if (port && (port < 1 || port > 65535)) {
|
|
22
|
+
errors.push('PORT must be between 1 and 65535')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const dbPort = Number(process.env.DB_PORT)
|
|
26
|
+
if (dbPort && (dbPort < 1 || dbPort > 65535)) {
|
|
27
|
+
errors.push('DB_PORT must be between 1 and 65535')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const redisPort = Number(process.env.REDIS_PORT)
|
|
31
|
+
if (redisPort && (redisPort < 1 || redisPort > 65535)) {
|
|
32
|
+
errors.push('REDIS_PORT must be between 1 and 65535')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
isValid: errors.length === 0,
|
|
37
|
+
errors
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function checkOIDCConfig() {
|
|
42
|
+
const issuer = process.env.OIDC_ISSUER
|
|
43
|
+
const jwksUri = process.env.OIDC_JWKS_URI
|
|
44
|
+
if (!issuer && !jwksUri) {
|
|
45
|
+
logger.error('[config] OIDC_ISSUER or OIDC_JWKS_URI must be set for jwks auth')
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
if (process.env.NODE_ENV === 'production' && !process.env.OIDC_AUDIENCE) {
|
|
49
|
+
logger.warn('[config] OIDC_AUDIENCE is not set; audience verification will be skipped')
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkRequiredEnv() {
|
|
55
|
+
const result = validateConfig()
|
|
56
|
+
|
|
57
|
+
if (result.errors.length > 0) {
|
|
58
|
+
logger.error('[config] Configuration validation failed:')
|
|
59
|
+
result.errors.forEach((err) => logger.error(` - ${err}`))
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.info('[config] Configuration validation passed')
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { validateConfig, checkOIDCConfig, checkRequiredEnv }
|
|
@@ -13,10 +13,6 @@ function validateConfig() {
|
|
|
13
13
|
logger.warn('[config] DB_USER not set, using default: root')
|
|
14
14
|
}
|
|
15
15
|
|
|
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
16
|
if (!process.env.REDIS_HOST) {
|
|
21
17
|
logger.warn('[config] REDIS_HOST not set, using default: 127.0.0.1')
|
|
22
18
|
}
|
|
@@ -42,24 +38,28 @@ function validateConfig() {
|
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
40
|
|
|
41
|
+
function checkJwtSecret() {
|
|
42
|
+
if (process.env.JWT_SECRET === 'please-change-this-in-production' || !process.env.JWT_SECRET) {
|
|
43
|
+
if (process.env.NODE_ENV === 'production') {
|
|
44
|
+
logger.error('[config] JWT_SECRET must be set in production!')
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
logger.warn('[config] WARNING: JWT_SECRET is using default value, please change in production!')
|
|
48
|
+
}
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
function checkRequiredEnv() {
|
|
46
53
|
const result = validateConfig()
|
|
47
|
-
|
|
54
|
+
|
|
48
55
|
if (result.errors.length > 0) {
|
|
49
56
|
logger.error('[config] Configuration validation failed:')
|
|
50
57
|
result.errors.forEach((err) => logger.error(` - ${err}`))
|
|
51
58
|
return false
|
|
52
59
|
}
|
|
53
60
|
|
|
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
61
|
logger.info('[config] Configuration validation passed')
|
|
62
62
|
return true
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
module.exports = { validateConfig, checkRequiredEnv }
|
|
65
|
+
module.exports = { validateConfig, checkJwtSecret, checkRequiredEnv }
|
|
@@ -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
|
|
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.
|
|
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.
|
|
32
|
-
"skweb-plugin": "^2.
|
|
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,137 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import express from 'skweb-express'
|
|
6
|
+
import expressLogger, { errorHandler as expressErrorHandler } from 'skweb-express-logger'
|
|
7
|
+
import swaggerUi from 'swagger-ui-express'
|
|
8
|
+
import logger from './utils/logger.js'
|
|
9
|
+
import { checkRequiredEnv, checkOIDCConfig } from './utils/config.js'
|
|
10
|
+
import { buildOpenApiSpec } from './swagger.js'
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
13
|
+
const __dirname = path.dirname(__filename)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* JWT 配置(jwks 模式,通过 OIDC 兼容的 JWKS 端点验证 RS256 签名)。
|
|
17
|
+
*/
|
|
18
|
+
function buildJwtOptions() {
|
|
19
|
+
const issuer = process.env.OIDC_ISSUER
|
|
20
|
+
const jwksUri = process.env.OIDC_JWKS_URI
|
|
21
|
+
const audience = process.env.OIDC_AUDIENCE
|
|
22
|
+
if (!issuer && !jwksUri) {
|
|
23
|
+
throw new Error('jwks mode requires OIDC_ISSUER or OIDC_JWKS_URI')
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
provider: 'jwks',
|
|
27
|
+
algorithms: ['RS256'],
|
|
28
|
+
jwks: { jwksUri, issuer, audience },
|
|
29
|
+
verify: { issuer, audience },
|
|
30
|
+
requestProperty: 'resolvedToken'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shouldEnableSwagger() {
|
|
35
|
+
const env = (process.env.NODE_ENV || '').toLowerCase()
|
|
36
|
+
if (env !== 'development' && env !== 'test') return false
|
|
37
|
+
if ((process.env.SWAGGER_ENABLED || '').toLowerCase() === 'false') return false
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function bootstrap() {
|
|
42
|
+
logger.info('[app] Initializing web process...')
|
|
43
|
+
|
|
44
|
+
if (!checkRequiredEnv()) {
|
|
45
|
+
logger.error('[app] Configuration validation failed, exiting...')
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
if (!checkOIDCConfig()) {
|
|
49
|
+
logger.error('[app] OIDC configuration validation failed, exiting...')
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { WebFramework } = await import('skweb-framework')
|
|
54
|
+
|
|
55
|
+
const framework = new WebFramework({
|
|
56
|
+
db: {
|
|
57
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
58
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
59
|
+
username: process.env.DB_USER || 'root',
|
|
60
|
+
password: process.env.DB_PASS || '',
|
|
61
|
+
database: process.env.DB_NAME || 'test-project',
|
|
62
|
+
dialect: process.env.DB_DIALECT || 'mysql',
|
|
63
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
64
|
+
underscored: true,
|
|
65
|
+
logging: process.env.DEBUG_DB === 'true' ? (msg) => logger.debug(msg) : false
|
|
66
|
+
},
|
|
67
|
+
redis: {
|
|
68
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
69
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
70
|
+
password: process.env.REDIS_PASSWORD || undefined
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const app = framework.createApp()
|
|
75
|
+
|
|
76
|
+
await framework.setup({
|
|
77
|
+
modelsDir: path.join(__dirname, './models'),
|
|
78
|
+
controllersDir: path.join(__dirname, './controllers')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
app.disable('x-powered-by')
|
|
82
|
+
app.disable('etag')
|
|
83
|
+
|
|
84
|
+
app.set('trust proxy', true)
|
|
85
|
+
|
|
86
|
+
app.set('wrap response', true)
|
|
87
|
+
app.setOkCode(0)
|
|
88
|
+
app.setOkMessage('success')
|
|
89
|
+
|
|
90
|
+
const jwtOptions = buildJwtOptions()
|
|
91
|
+
app.setJWT('default', jwtOptions)
|
|
92
|
+
app.setJwtHintHeader('JwtHint')
|
|
93
|
+
|
|
94
|
+
logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks.issuer || jwtOptions.jwks.jwksUri})`)
|
|
95
|
+
|
|
96
|
+
app.use(expressLogger())
|
|
97
|
+
app.use(express.static(path.join(__dirname, '..', 'public')))
|
|
98
|
+
|
|
99
|
+
if (shouldEnableSwagger()) {
|
|
100
|
+
const openapiSpec = buildOpenApiSpec()
|
|
101
|
+
app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
|
|
102
|
+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpec, {
|
|
103
|
+
customSiteTitle: `${process.env.APP_NAME || 'SKWeb'} API Docs`,
|
|
104
|
+
swaggerOptions: { persistAuthorization: true }
|
|
105
|
+
}))
|
|
106
|
+
logger.info(`[app] Swagger UI mounted at http://localhost:${process.env.PORT || 3000}/api-docs (env: ${process.env.NODE_ENV})`)
|
|
107
|
+
} else {
|
|
108
|
+
logger.info(`[app] Swagger UI disabled (env: ${process.env.NODE_ENV || 'unset'})`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await framework.loadControllers()
|
|
112
|
+
|
|
113
|
+
app.use(expressErrorHandler())
|
|
114
|
+
|
|
115
|
+
framework.start(Number(process.env.PORT) || 3000)
|
|
116
|
+
|
|
117
|
+
logger.info('[app] Initialization complete (web process)')
|
|
118
|
+
|
|
119
|
+
process.on('uncaughtException', (err) => {
|
|
120
|
+
logger.error(`[app] uncaughtException: ${err.stack || err.message}`)
|
|
121
|
+
process.exit(1)
|
|
122
|
+
})
|
|
123
|
+
process.on('unhandledRejection', (reason) => {
|
|
124
|
+
logger.error(`[app] unhandledRejection: ${reason instanceof Error ? reason.stack : String(reason)}`)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`
|
|
130
|
+
if (isMainModule) {
|
|
131
|
+
bootstrap().catch((err) => {
|
|
132
|
+
logger.error('[app] Fatal error:', err)
|
|
133
|
+
process.exit(1)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { bootstrap }
|
|
@@ -6,36 +6,16 @@ import express from 'skweb-express'
|
|
|
6
6
|
import expressLogger, { errorHandler as expressErrorHandler } from 'skweb-express-logger'
|
|
7
7
|
import swaggerUi from 'swagger-ui-express'
|
|
8
8
|
import logger from './utils/logger.js'
|
|
9
|
-
import { checkRequiredEnv } from './utils/config.js'
|
|
9
|
+
import { checkRequiredEnv, checkJwtSecret } from './utils/config.js'
|
|
10
10
|
import { buildOpenApiSpec } from './swagger.js'
|
|
11
11
|
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url)
|
|
13
13
|
const __dirname = path.dirname(__filename)
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* - 'static' (默认):使用共享密钥(HS256/HS384/HS512),适用于自签 token
|
|
18
|
-
* - 'jwks':通过 JWKS 端点动态获取公钥(RS256 等),适用于 Keycloak/Auth0 等 IdP
|
|
16
|
+
* JWT 配置(static 模式,HS256 共享密钥)。
|
|
19
17
|
*/
|
|
20
18
|
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
19
|
return {
|
|
40
20
|
provider: 'static',
|
|
41
21
|
secret: process.env.JWT_SECRET || 'please-change-this-in-production',
|
|
@@ -45,10 +25,6 @@ function buildJwtOptions() {
|
|
|
45
25
|
}
|
|
46
26
|
}
|
|
47
27
|
|
|
48
|
-
/**
|
|
49
|
-
* Swagger UI 仅在 NODE_ENV 为 development / test,且 SWAGGER_ENABLED !== 'false' 时启用。
|
|
50
|
-
* 生产环境(production / unset)默认禁用,避免暴露内部 API 细节。
|
|
51
|
-
*/
|
|
52
28
|
function shouldEnableSwagger() {
|
|
53
29
|
const env = (process.env.NODE_ENV || '').toLowerCase()
|
|
54
30
|
if (env !== 'development' && env !== 'test') return false
|
|
@@ -63,6 +39,10 @@ async function bootstrap() {
|
|
|
63
39
|
logger.error('[app] Configuration validation failed, exiting...')
|
|
64
40
|
process.exit(1)
|
|
65
41
|
}
|
|
42
|
+
if (!checkJwtSecret()) {
|
|
43
|
+
logger.error('[app] JWT configuration validation failed, exiting...')
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
66
46
|
|
|
67
47
|
const { WebFramework } = await import('skweb-framework')
|
|
68
48
|
|
|
@@ -101,21 +81,15 @@ async function bootstrap() {
|
|
|
101
81
|
app.setOkCode(0)
|
|
102
82
|
app.setOkMessage('success')
|
|
103
83
|
|
|
104
|
-
// JWT 配置(按 AUTH_PROVIDER 选择 static 或 jwks 模式)
|
|
105
84
|
const jwtOptions = buildJwtOptions()
|
|
106
85
|
app.setJWT('default', jwtOptions)
|
|
107
86
|
app.setJwtHintHeader('JwtHint')
|
|
108
87
|
|
|
109
|
-
|
|
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
|
-
}
|
|
88
|
+
logger.info('[app] JWT auth provider: static (HS256)')
|
|
114
89
|
|
|
115
90
|
app.use(expressLogger())
|
|
116
91
|
app.use(express.static(path.join(__dirname, '..', 'public')))
|
|
117
92
|
|
|
118
|
-
// 仅在 dev / test 环境挂载 Swagger UI(默认关闭,需显式 SWAGGER_ENABLED=true)
|
|
119
93
|
if (shouldEnableSwagger()) {
|
|
120
94
|
const openapiSpec = buildOpenApiSpec()
|
|
121
95
|
app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
|
|
@@ -130,14 +104,12 @@ async function bootstrap() {
|
|
|
130
104
|
|
|
131
105
|
await framework.loadControllers()
|
|
132
106
|
|
|
133
|
-
// 错误日志中间件:挂在所有路由之后,记录业务错误并继续传递给默认错误处理。
|
|
134
107
|
app.use(expressErrorHandler())
|
|
135
108
|
|
|
136
109
|
framework.start(Number(process.env.PORT) || 3000)
|
|
137
110
|
|
|
138
111
|
logger.info('[app] Initialization complete (web process)')
|
|
139
112
|
|
|
140
|
-
// 未捕获异常:打印堆栈后直接退出。OS / 进程管理器负责回收资源。
|
|
141
113
|
process.on('uncaughtException', (err) => {
|
|
142
114
|
logger.error(`[app] uncaughtException: ${err.stack || err.message}`)
|
|
143
115
|
process.exit(1)
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
export function buildOpenApiSpec() {
|
|
9
|
+
return swaggerJSDoc({
|
|
10
|
+
definition: {
|
|
11
|
+
openapi: '3.0.3',
|
|
12
|
+
info: {
|
|
13
|
+
title: process.env.APP_NAME || 'SKWeb API',
|
|
14
|
+
version: process.env.API_VERSION || '1.0.0',
|
|
15
|
+
description: process.env.APP_DESCRIPTION || 'SKWeb project API documentation',
|
|
16
|
+
contact: { name: 'API Support' }
|
|
17
|
+
},
|
|
18
|
+
servers: [
|
|
19
|
+
{ url: `http://localhost:${process.env.PORT || 3000}`, description: 'Local development' }
|
|
20
|
+
],
|
|
21
|
+
components: {
|
|
22
|
+
securitySchemes: {
|
|
23
|
+
oidcAuth: {
|
|
24
|
+
type: 'openIdConnect',
|
|
25
|
+
openIdConnectUrl: process.env.OIDC_JWKS_URI
|
|
26
|
+
? process.env.OIDC_JWKS_URI.replace(/\/jwks\.json$/, '/.well-known/openid-configuration')
|
|
27
|
+
: (process.env.OIDC_ISSUER
|
|
28
|
+
? `${process.env.OIDC_ISSUER.replace(/\/$/, '')}/.well-known/openid-configuration`
|
|
29
|
+
: undefined)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
apis: [
|
|
35
|
+
path.join(__dirname, 'controllers', '*.js')
|
|
36
|
+
]
|
|
37
|
+
})
|
|
38
|
+
}
|
|
@@ -5,22 +5,6 @@ import swaggerJSDoc from 'swagger-jsdoc'
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url)
|
|
6
6
|
const __dirname = path.dirname(__filename)
|
|
7
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
8
|
export function buildOpenApiSpec() {
|
|
25
9
|
return swaggerJSDoc({
|
|
26
10
|
definition: {
|
|
@@ -39,8 +23,7 @@ export function buildOpenApiSpec() {
|
|
|
39
23
|
bearerAuth: {
|
|
40
24
|
type: 'http',
|
|
41
25
|
scheme: 'bearer',
|
|
42
|
-
bearerFormat: 'JWT'
|
|
43
|
-
description: 'Paste a JWT token here (only needed when AUTH_PROVIDER=static)'
|
|
26
|
+
bearerFormat: 'JWT'
|
|
44
27
|
}
|
|
45
28
|
}
|
|
46
29
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import logger from './logger.js'
|
|
2
|
+
|
|
3
|
+
export function validateConfig() {
|
|
4
|
+
const errors = []
|
|
5
|
+
|
|
6
|
+
if (!process.env.DB_HOST) {
|
|
7
|
+
logger.warn('[config] DB_HOST not set, using default: 127.0.0.1')
|
|
8
|
+
}
|
|
9
|
+
if (!process.env.DB_NAME) {
|
|
10
|
+
logger.warn('[config] DB_NAME not set, using default: test-project')
|
|
11
|
+
}
|
|
12
|
+
if (!process.env.DB_USER) {
|
|
13
|
+
logger.warn('[config] DB_USER not set, using default: root')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!process.env.REDIS_HOST) {
|
|
17
|
+
logger.warn('[config] REDIS_HOST not set, using default: 127.0.0.1')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const port = Number(process.env.PORT)
|
|
21
|
+
if (port && (port < 1 || port > 65535)) {
|
|
22
|
+
errors.push('PORT must be between 1 and 65535')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const dbPort = Number(process.env.DB_PORT)
|
|
26
|
+
if (dbPort && (dbPort < 1 || dbPort > 65535)) {
|
|
27
|
+
errors.push('DB_PORT must be between 1 and 65535')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const redisPort = Number(process.env.REDIS_PORT)
|
|
31
|
+
if (redisPort && (redisPort < 1 || redisPort > 65535)) {
|
|
32
|
+
errors.push('REDIS_PORT must be between 1 and 65535')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
isValid: errors.length === 0,
|
|
37
|
+
errors
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function checkOIDCConfig() {
|
|
42
|
+
const issuer = process.env.OIDC_ISSUER
|
|
43
|
+
const jwksUri = process.env.OIDC_JWKS_URI
|
|
44
|
+
if (!issuer && !jwksUri) {
|
|
45
|
+
logger.error('[config] OIDC_ISSUER or OIDC_JWKS_URI must be set for jwks auth')
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
if (process.env.NODE_ENV === 'production' && !process.env.OIDC_AUDIENCE) {
|
|
49
|
+
logger.warn('[config] OIDC_AUDIENCE is not set; audience verification will be skipped')
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function checkRequiredEnv() {
|
|
55
|
+
const result = validateConfig()
|
|
56
|
+
|
|
57
|
+
if (result.errors.length > 0) {
|
|
58
|
+
logger.error('[config] Configuration validation failed:')
|
|
59
|
+
result.errors.forEach((err) => logger.error(` - ${err}`))
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.info('[config] Configuration validation passed')
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
@@ -13,10 +13,6 @@ export function validateConfig() {
|
|
|
13
13
|
logger.warn('[config] DB_USER not set, using default: root')
|
|
14
14
|
}
|
|
15
15
|
|
|
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
16
|
if (!process.env.REDIS_HOST) {
|
|
21
17
|
logger.warn('[config] REDIS_HOST not set, using default: 127.0.0.1')
|
|
22
18
|
}
|
|
@@ -42,22 +38,26 @@ export function validateConfig() {
|
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
40
|
|
|
41
|
+
export function checkJwtSecret() {
|
|
42
|
+
if (process.env.JWT_SECRET === 'please-change-this-in-production' || !process.env.JWT_SECRET) {
|
|
43
|
+
if (process.env.NODE_ENV === 'production') {
|
|
44
|
+
logger.error('[config] JWT_SECRET must be set in production!')
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
logger.warn('[config] WARNING: JWT_SECRET is using default value, please change in production!')
|
|
48
|
+
}
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
export function checkRequiredEnv() {
|
|
46
53
|
const result = validateConfig()
|
|
47
|
-
|
|
54
|
+
|
|
48
55
|
if (result.errors.length > 0) {
|
|
49
56
|
logger.error('[config] Configuration validation failed:')
|
|
50
57
|
result.errors.forEach((err) => logger.error(` - ${err}`))
|
|
51
58
|
return false
|
|
52
59
|
}
|
|
53
60
|
|
|
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
61
|
logger.info('[config] Configuration validation passed')
|
|
62
62
|
return true
|
|
63
|
-
}
|
|
63
|
+
}
|
|
@@ -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
|
|
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.
|