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
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,4 +1,69 @@
|
|
|
1
1
|
|
|
2
|
-
> create-skweb@
|
|
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,
|
|
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,
|
|
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
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,
|
|
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
|
|
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,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
|
-
*
|
|
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
|
-
|
|
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
|
}
|