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.
- package/.turbo/turbo-build.log +18 -0
- package/.turbo/turbo-check.log +4 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +4 -0
- package/CHANGELOG.md +20 -5
- package/dist/cli.cjs +75 -25
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +63 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +12 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +68 -27
- package/src/index.ts +13 -5
- package/templates/cjs/.env.example +73 -0
- package/templates/cjs/package.json +4 -2
- package/templates/cjs/src/app.js +69 -7
- package/templates/cjs/src/controllers/hello.js +46 -0
- package/templates/cjs/src/swagger.js +52 -0
- package/templates/mjs/.env.example +73 -0
- package/templates/mjs/package.json +4 -2
- package/templates/mjs/src/app.js +69 -7
- package/templates/mjs/src/controllers/hello.js +46 -0
- package/templates/mjs/src/swagger.js +52 -0
- package/templates/ts/.env.example +73 -0
- package/templates/ts/package.json +6 -2
- package/templates/ts/src/app.ts +77 -7
- package/templates/ts/src/controllers/hello.ts +46 -0
- package/templates/ts/src/swagger.ts +54 -0
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { execSync } from 'child_process';
|
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
async function createProject(options) {
|
|
8
8
|
const { name, description = 'A SKWeb project', template, dest } = options;
|
|
9
|
+
const auth = options.auth ?? 'static';
|
|
9
10
|
const templateDir = path.join(__dirname, '..', 'templates', template);
|
|
10
11
|
const baseDir = path.resolve(dest);
|
|
11
12
|
const targetDir = path.join(baseDir, name);
|
|
@@ -29,7 +30,8 @@ async function createProject(options) {
|
|
|
29
30
|
let content = await fs.readFile(srcPath, 'utf-8');
|
|
30
31
|
content = content
|
|
31
32
|
.replace(/\{\{name\}\}/g, name)
|
|
32
|
-
.replace(/\{\{description\}\}/g, description)
|
|
33
|
+
.replace(/\{\{description\}\}/g, description)
|
|
34
|
+
.replace(/\{\{auth\}\}/g, auth);
|
|
33
35
|
const destPath = path.join(targetDir, fileName);
|
|
34
36
|
await fs.writeFile(destPath, content);
|
|
35
37
|
}
|
|
@@ -73,20 +75,23 @@ function printSuccess(name, dest) {
|
|
|
73
75
|
接下来可以执行:
|
|
74
76
|
|
|
75
77
|
cd ${name}
|
|
76
|
-
|
|
78
|
+
|
|
79
|
+
# 复制环境变量模板并按需修改
|
|
80
|
+
cp .env.example .env
|
|
81
|
+
|
|
77
82
|
# 开发模式运行
|
|
78
83
|
npm run start:dev
|
|
79
|
-
|
|
84
|
+
|
|
80
85
|
# 生产模式运行 (需要先构建)
|
|
81
86
|
npm run build
|
|
82
87
|
npm run start
|
|
83
|
-
|
|
88
|
+
|
|
84
89
|
# 查看日志
|
|
85
90
|
npm run logs
|
|
86
|
-
|
|
91
|
+
|
|
87
92
|
# 停止服务
|
|
88
93
|
npm run stop
|
|
89
|
-
|
|
94
|
+
|
|
90
95
|
# 重启服务
|
|
91
96
|
npm run restart
|
|
92
97
|
|
|
@@ -95,6 +100,7 @@ function printSuccess(name, dest) {
|
|
|
95
100
|
✅ @dotenvx/dotenvx 环境变量管理
|
|
96
101
|
✅ PM2 进程管理
|
|
97
102
|
✅ SKWeb Express 集成
|
|
103
|
+
✅ Keycloak/JWKS 鉴权 (可选)
|
|
98
104
|
✅ ESLint 代码检查
|
|
99
105
|
✅ Mocha 测试框架
|
|
100
106
|
`);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;;AAKA,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -20,14 +20,31 @@ async function main() {
|
|
|
20
20
|
.argument('[name]', 'Project name')
|
|
21
21
|
.option('--template <type>', 'Template type: ts, mjs, cjs', 'ts')
|
|
22
22
|
.option('--dest <path>', 'Destination directory', '.')
|
|
23
|
+
.option('--auth <type>', 'Auth provider: static (HS256) or jwks (Keycloak/Auth0)', 'static')
|
|
23
24
|
.option('--skip-install', 'Skip dependency installation')
|
|
24
25
|
.option('--skip-git', 'Skip git initialization')
|
|
25
26
|
.action(async (name, options) => {
|
|
26
27
|
printBanner()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
|
|
29
|
+
// 只要没有传 --no-interactive 且任何一个关键参数缺失,就走交互式 prompt
|
|
30
|
+
const providedTemplate = process.argv.includes('--template')
|
|
31
|
+
const providedDest = process.argv.includes('--dest')
|
|
32
|
+
const providedAuth = process.argv.includes('--auth')
|
|
33
|
+
const providedSkipInstall = process.argv.includes('--skip-install')
|
|
34
|
+
const providedSkipGit = process.argv.includes('--skip-git')
|
|
35
|
+
|
|
36
|
+
// 如果有任何一个关键参数未通过命令行提供,就走交互式 prompt
|
|
37
|
+
const needsPrompt = !name
|
|
38
|
+
|| !providedTemplate
|
|
39
|
+
|| !providedDest
|
|
40
|
+
|| !providedAuth
|
|
41
|
+
|| !providedSkipInstall
|
|
42
|
+
|| !providedSkipGit
|
|
43
|
+
|
|
44
|
+
if (needsPrompt) {
|
|
45
|
+
const questions: any[] = []
|
|
46
|
+
if (!name) {
|
|
47
|
+
questions.push({
|
|
31
48
|
type: 'input',
|
|
32
49
|
name: 'name',
|
|
33
50
|
message: 'Enter project name:',
|
|
@@ -38,8 +55,10 @@ async function main() {
|
|
|
38
55
|
}
|
|
39
56
|
return true
|
|
40
57
|
}
|
|
41
|
-
}
|
|
42
|
-
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
if (!providedTemplate) {
|
|
61
|
+
questions.push({
|
|
43
62
|
type: 'list',
|
|
44
63
|
name: 'template',
|
|
45
64
|
message: 'Select project template:',
|
|
@@ -49,44 +68,66 @@ async function main() {
|
|
|
49
68
|
{ name: 'CommonJS (CJS)', value: 'cjs' }
|
|
50
69
|
],
|
|
51
70
|
default: 'ts'
|
|
52
|
-
}
|
|
53
|
-
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
if (!providedAuth) {
|
|
74
|
+
questions.push({
|
|
75
|
+
type: 'list',
|
|
76
|
+
name: 'auth',
|
|
77
|
+
message: 'Select auth provider:',
|
|
78
|
+
choices: [
|
|
79
|
+
{ name: 'Static (HS256 shared secret)', value: 'static' },
|
|
80
|
+
{ name: 'JWKS (Keycloak / Auth0 / OIDC)', value: 'jwks' }
|
|
81
|
+
],
|
|
82
|
+
default: 'static'
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
if (!providedDest) {
|
|
86
|
+
questions.push({
|
|
54
87
|
type: 'input',
|
|
55
88
|
name: 'dest',
|
|
56
89
|
message: 'Enter destination directory:',
|
|
57
90
|
default: '.'
|
|
58
|
-
}
|
|
59
|
-
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
if (!providedSkipInstall) {
|
|
94
|
+
questions.push({
|
|
60
95
|
type: 'confirm',
|
|
61
96
|
name: 'install',
|
|
62
97
|
message: 'Install dependencies?',
|
|
63
98
|
default: true
|
|
64
|
-
}
|
|
65
|
-
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
if (!providedSkipGit) {
|
|
102
|
+
questions.push({
|
|
66
103
|
type: 'confirm',
|
|
67
104
|
name: 'git',
|
|
68
105
|
message: 'Initialize git repository?',
|
|
69
106
|
default: true
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
options.
|
|
77
|
-
options.
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const answers: any = await inquirer.prompt(questions)
|
|
111
|
+
|
|
112
|
+
if (answers.name) name = answers.name
|
|
113
|
+
if (answers.template) options.template = answers.template
|
|
114
|
+
if (answers.auth) options.auth = answers.auth
|
|
115
|
+
if (answers.dest) options.dest = answers.dest
|
|
116
|
+
if (typeof answers.install === 'boolean') options.skipInstall = !answers.install
|
|
117
|
+
if (typeof answers.git === 'boolean') options.skipGit = !answers.git
|
|
78
118
|
}
|
|
79
|
-
|
|
119
|
+
|
|
80
120
|
const spinner = ora('Creating project...').start()
|
|
81
|
-
|
|
121
|
+
|
|
82
122
|
try {
|
|
83
123
|
await createProject({
|
|
84
124
|
name,
|
|
85
125
|
template: options.template as 'ts' | 'mjs' | 'cjs',
|
|
86
|
-
dest: options.dest
|
|
126
|
+
dest: options.dest,
|
|
127
|
+
auth: (options.auth || 'static') as 'static' | 'jwks'
|
|
87
128
|
})
|
|
88
129
|
spinner.succeed('Project files created')
|
|
89
|
-
|
|
130
|
+
|
|
90
131
|
if (!options.skipInstall) {
|
|
91
132
|
spinner.text = 'Installing dependencies...'
|
|
92
133
|
spinner.start()
|
|
@@ -100,11 +141,11 @@ async function main() {
|
|
|
100
141
|
await initGit(options.dest, name)
|
|
101
142
|
spinner.succeed('Git repository initialized')
|
|
102
143
|
}
|
|
103
|
-
|
|
144
|
+
|
|
104
145
|
spinner.stop()
|
|
105
|
-
|
|
146
|
+
|
|
106
147
|
printSuccess(name, options.dest)
|
|
107
|
-
|
|
148
|
+
|
|
108
149
|
} catch (error) {
|
|
109
150
|
spinner.fail('Failed to create project')
|
|
110
151
|
console.error(error)
|
package/src/index.ts
CHANGED
|
@@ -10,10 +10,13 @@ export interface CreateOptions {
|
|
|
10
10
|
description?: string
|
|
11
11
|
template: 'ts' | 'mjs' | 'cjs'
|
|
12
12
|
dest: string
|
|
13
|
+
/** Auth provider: 'static' (HS256, default) or 'jwks' (Keycloak/Auth0) */
|
|
14
|
+
auth?: 'static' | 'jwks'
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export async function createProject(options: CreateOptions): Promise<void> {
|
|
16
18
|
const { name, description = 'A SKWeb project', template, dest } = options
|
|
19
|
+
const auth = options.auth ?? 'static'
|
|
17
20
|
|
|
18
21
|
const templateDir = path.join(__dirname, '..', 'templates', template)
|
|
19
22
|
const baseDir = path.resolve(dest)
|
|
@@ -44,6 +47,7 @@ export async function createProject(options: CreateOptions): Promise<void> {
|
|
|
44
47
|
content = content
|
|
45
48
|
.replace(/\{\{name\}\}/g, name)
|
|
46
49
|
.replace(/\{\{description\}\}/g, description)
|
|
50
|
+
.replace(/\{\{auth\}\}/g, auth)
|
|
47
51
|
|
|
48
52
|
const destPath = path.join(targetDir, fileName)
|
|
49
53
|
await fs.writeFile(destPath, content)
|
|
@@ -96,20 +100,23 @@ export function printSuccess(name: string, dest: string): void {
|
|
|
96
100
|
接下来可以执行:
|
|
97
101
|
|
|
98
102
|
cd ${name}
|
|
99
|
-
|
|
103
|
+
|
|
104
|
+
# 复制环境变量模板并按需修改
|
|
105
|
+
cp .env.example .env
|
|
106
|
+
|
|
100
107
|
# 开发模式运行
|
|
101
108
|
npm run start:dev
|
|
102
|
-
|
|
109
|
+
|
|
103
110
|
# 生产模式运行 (需要先构建)
|
|
104
111
|
npm run build
|
|
105
112
|
npm run start
|
|
106
|
-
|
|
113
|
+
|
|
107
114
|
# 查看日志
|
|
108
115
|
npm run logs
|
|
109
|
-
|
|
116
|
+
|
|
110
117
|
# 停止服务
|
|
111
118
|
npm run stop
|
|
112
|
-
|
|
119
|
+
|
|
113
120
|
# 重启服务
|
|
114
121
|
npm run restart
|
|
115
122
|
|
|
@@ -118,6 +125,7 @@ export function printSuccess(name: string, dest: string): void {
|
|
|
118
125
|
✅ @dotenvx/dotenvx 环境变量管理
|
|
119
126
|
✅ PM2 进程管理
|
|
120
127
|
✅ SKWeb Express 集成
|
|
128
|
+
✅ Keycloak/JWKS 鉴权 (可选)
|
|
121
129
|
✅ ESLint 代码检查
|
|
122
130
|
✅ Mocha 测试框架
|
|
123
131
|
`)
|
|
@@ -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=*
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@dotenvx/dotenvx": "^1.71.0",
|
|
24
24
|
"http-errors": "^2.0.0",
|
|
25
25
|
"skweb-express": "^1.0.0",
|
|
26
|
-
"skweb-express-jwt": "^1.
|
|
26
|
+
"skweb-express-jwt": "^1.2.0",
|
|
27
27
|
"skweb-express-logger": "^1.0.0",
|
|
28
28
|
"skweb-sequelize": "^1.0.0",
|
|
29
29
|
"skweb-redis": "^1.0.0",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"skweb-plugin": "^1.0.0",
|
|
33
33
|
"skweb-util": "^1.0.0",
|
|
34
34
|
"winston": "^3.17.0",
|
|
35
|
-
"sequelize": "^6.37.7"
|
|
35
|
+
"sequelize": "^6.37.7",
|
|
36
|
+
"swagger-jsdoc": "^6.2.8",
|
|
37
|
+
"swagger-ui-express": "^5.0.1"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"chai": "^6.2.1",
|
package/templates/cjs/src/app.js
CHANGED
|
@@ -3,9 +3,55 @@ require('dotenv/config')
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const express = require('skweb-express')
|
|
5
5
|
const expressLogger = require('skweb-express-logger').default || require('skweb-express-logger')
|
|
6
|
+
const swaggerUi = require('swagger-ui-express')
|
|
6
7
|
const { WebFramework } = require('skweb-framework')
|
|
7
8
|
const { logger } = require('./utils/logger.js')
|
|
8
9
|
const { checkRequiredEnv } = require('./utils/config.js')
|
|
10
|
+
const { buildOpenApiSpec } = require('./swagger.js')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 构建 JWT 配置,支持两种模式:
|
|
14
|
+
* - 'static' (默认):使用共享密钥(HS256/HS384/HS512),适用于自签 token
|
|
15
|
+
* - 'jwks':通过 JWKS 端点动态获取公钥(RS256 等),适用于 Keycloak/Auth0 等 IdP
|
|
16
|
+
*/
|
|
17
|
+
function buildJwtOptions() {
|
|
18
|
+
const provider = process.env.AUTH_PROVIDER || 'static'
|
|
19
|
+
|
|
20
|
+
if (provider === 'jwks') {
|
|
21
|
+
const issuer = process.env.OIDC_ISSUER
|
|
22
|
+
const jwksUri = process.env.OIDC_JWKS_URI
|
|
23
|
+
const audience = process.env.OIDC_AUDIENCE
|
|
24
|
+
if (!issuer && !jwksUri) {
|
|
25
|
+
throw new Error('AUTH_PROVIDER=jwks requires OIDC_ISSUER or OIDC_JWKS_URI')
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
provider: 'jwks',
|
|
29
|
+
algorithms: ['RS256'],
|
|
30
|
+
jwks: { jwksUri, issuer, audience },
|
|
31
|
+
verify: { issuer, audience },
|
|
32
|
+
requestProperty: 'resolvedToken'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
provider: 'static',
|
|
38
|
+
secret: process.env.JWT_SECRET || 'please-change-this-in-production',
|
|
39
|
+
algorithms: ['HS256'],
|
|
40
|
+
sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
|
|
41
|
+
requestProperty: 'resolvedToken'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Swagger UI 仅在 NODE_ENV 为 development / test,且 SWAGGER_ENABLED !== 'false' 时启用。
|
|
47
|
+
* 生产环境(production / unset)默认禁用,避免暴露内部 API 细节。
|
|
48
|
+
*/
|
|
49
|
+
function shouldEnableSwagger() {
|
|
50
|
+
const env = (process.env.NODE_ENV || '').toLowerCase()
|
|
51
|
+
if (env !== 'development' && env !== 'test') return false
|
|
52
|
+
if ((process.env.SWAGGER_ENABLED || '').toLowerCase() === 'false') return false
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
9
55
|
|
|
10
56
|
async function bootstrap() {
|
|
11
57
|
logger.info('[app] Initializing web process...')
|
|
@@ -50,17 +96,33 @@ async function bootstrap() {
|
|
|
50
96
|
app.setOkCode(0)
|
|
51
97
|
app.setOkMessage('success')
|
|
52
98
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
|
|
57
|
-
requestProperty: 'resolvedToken'
|
|
58
|
-
})
|
|
99
|
+
// JWT 配置(按 AUTH_PROVIDER 选择 static 或 jwks 模式)
|
|
100
|
+
const jwtOptions = buildJwtOptions()
|
|
101
|
+
app.setJWT('default', jwtOptions)
|
|
59
102
|
app.setJwtHintHeader('JwtHint')
|
|
60
103
|
|
|
104
|
+
if (jwtOptions.provider === 'jwks') {
|
|
105
|
+
logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks.issuer || jwtOptions.jwks.jwksUri})`)
|
|
106
|
+
} else {
|
|
107
|
+
logger.info('[app] JWT auth provider: static (HS256)')
|
|
108
|
+
}
|
|
109
|
+
|
|
61
110
|
app.use(expressLogger())
|
|
62
111
|
app.use(express.static(path.join(__dirname, '..', 'public')))
|
|
63
112
|
|
|
113
|
+
// 仅在 dev / test 环境挂载 Swagger UI(默认关闭,需显式 SWAGGER_ENABLED=true)
|
|
114
|
+
if (shouldEnableSwagger()) {
|
|
115
|
+
const openapiSpec = buildOpenApiSpec()
|
|
116
|
+
app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
|
|
117
|
+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpec, {
|
|
118
|
+
customSiteTitle: `${process.env.APP_NAME || 'SKWeb'} API Docs`,
|
|
119
|
+
swaggerOptions: { persistAuthorization: true }
|
|
120
|
+
}))
|
|
121
|
+
logger.info(`[app] Swagger UI mounted at http://localhost:${process.env.PORT || 3000}/api-docs (env: ${process.env.NODE_ENV})`)
|
|
122
|
+
} else {
|
|
123
|
+
logger.info(`[app] Swagger UI disabled (env: ${process.env.NODE_ENV || 'unset'})`)
|
|
124
|
+
}
|
|
125
|
+
|
|
64
126
|
await framework.loadControllers(path.join(__dirname, './controllers'))
|
|
65
127
|
await framework.loadPluginControllers()
|
|
66
128
|
|
|
@@ -88,4 +150,4 @@ if (require.main === module) {
|
|
|
88
150
|
})
|
|
89
151
|
}
|
|
90
152
|
|
|
91
|
-
module.exports = { bootstrap }
|
|
153
|
+
module.exports = { 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
|
module.exports = [
|
|
2
48
|
{
|
|
3
49
|
method: 'get',
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const swaggerJSDoc = require('swagger-jsdoc')
|
|
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
|
+
function buildOpenApiSpec() {
|
|
23
|
+
return swaggerJSDoc({
|
|
24
|
+
definition: {
|
|
25
|
+
openapi: '3.0.3',
|
|
26
|
+
info: {
|
|
27
|
+
title: process.env.APP_NAME || 'SKWeb API',
|
|
28
|
+
version: process.env.API_VERSION || '1.0.0',
|
|
29
|
+
description: process.env.APP_DESCRIPTION || 'SKWeb project API documentation',
|
|
30
|
+
contact: { name: 'API Support' }
|
|
31
|
+
},
|
|
32
|
+
servers: [
|
|
33
|
+
{ url: `http://localhost:${process.env.PORT || 3000}`, description: 'Local development' }
|
|
34
|
+
],
|
|
35
|
+
components: {
|
|
36
|
+
securitySchemes: {
|
|
37
|
+
bearerAuth: {
|
|
38
|
+
type: 'http',
|
|
39
|
+
scheme: 'bearer',
|
|
40
|
+
bearerFormat: 'JWT',
|
|
41
|
+
description: 'Paste a JWT token here (only needed when AUTH_PROVIDER=static)'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
apis: [
|
|
47
|
+
path.join(__dirname, 'controllers', '*.js')
|
|
48
|
+
]
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { buildOpenApiSpec }
|
|
@@ -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=*
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@dotenvx/dotenvx": "^1.71.0",
|
|
24
24
|
"http-errors": "^2.0.0",
|
|
25
25
|
"skweb-express": "^1.0.0",
|
|
26
|
-
"skweb-express-jwt": "^1.
|
|
26
|
+
"skweb-express-jwt": "^1.2.0",
|
|
27
27
|
"skweb-express-logger": "^1.0.0",
|
|
28
28
|
"skweb-sequelize": "^1.0.0",
|
|
29
29
|
"skweb-redis": "^1.0.0",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"skweb-plugin": "^1.0.0",
|
|
33
33
|
"skweb-util": "^1.0.0",
|
|
34
34
|
"winston": "^3.17.0",
|
|
35
|
-
"sequelize": "^6.37.7"
|
|
35
|
+
"sequelize": "^6.37.7",
|
|
36
|
+
"swagger-jsdoc": "^6.2.8",
|
|
37
|
+
"swagger-ui-express": "^5.0.1"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"chai": "^6.2.1",
|