create-skweb 3.2.0 → 3.3.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 +11 -16
- package/.rollup.cache/Users/stayknight/projects/skweb-mono/tools/create-skweb/tsconfig.tsbuildinfo +1 -1
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +6 -0
- package/dist/cli.js +11 -16
- package/dist/index.js +11 -16
- package/package.json +1 -1
- package/src/index.ts +18 -16
- package/templates/cjs/{.env.example.auth-jwks → .env.example} +7 -0
- package/templates/cjs/ecosystem.config.js +2 -2
- package/templates/cjs/package.json +4 -4
- package/templates/cjs/src/controllers/user.js +2 -2
- package/templates/cjs/src/cron-app.js +40 -0
- package/templates/cjs/src/cron-framework.js +44 -0
- package/templates/cjs/src/{swagger.auth-jwks.js → swagger.js} +9 -0
- package/templates/cjs/src/utils/{config.auth-jwks.js → config.js} +18 -0
- package/templates/cjs/src/{app.auth-jwks.js → web-app.js} +34 -21
- package/templates/cjs/src/web-framework.js +23 -0
- package/templates/{ts/.env.example.auth-jwks → mjs/.env.example} +7 -0
- package/templates/mjs/ecosystem.config.js +2 -2
- package/templates/mjs/package.json +4 -4
- package/templates/mjs/src/controllers/user.js +2 -2
- package/templates/mjs/src/cron-app.js +45 -0
- package/templates/mjs/src/cron-framework.js +44 -0
- package/templates/mjs/src/{swagger.auth-jwks.js → swagger.js} +9 -0
- package/templates/mjs/src/utils/{config.auth-jwks.js → config.js} +14 -0
- package/templates/mjs/src/{app.auth-jwks.js → web-app.js} +29 -23
- package/templates/mjs/src/web-framework.js +23 -0
- package/templates/{mjs/.env.example.auth-jwks → ts/.env.example} +7 -0
- package/templates/ts/ecosystem.config.js +2 -2
- package/templates/ts/package.json +4 -4
- package/templates/ts/src/controllers/user.ts +2 -2
- package/templates/ts/src/cron-app.ts +45 -0
- package/templates/ts/src/cron-framework.ts +44 -0
- package/templates/ts/src/{swagger.auth-jwks.ts → swagger.ts} +9 -0
- package/templates/ts/src/utils/{config.auth-jwks.ts → config.ts} +14 -0
- package/templates/ts/src/{app.auth-jwks.ts → web-app.ts} +28 -21
- package/templates/ts/src/web-framework.ts +23 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/templates/cjs/.env.example.auth-static +0 -54
- package/templates/cjs/src/app.auth-static.js +0 -129
- package/templates/cjs/src/cronWorker.js +0 -80
- package/templates/cjs/src/index.js +0 -7
- package/templates/cjs/src/services/database.js +0 -15
- package/templates/cjs/src/swagger.auth-static.js +0 -35
- package/templates/cjs/src/utils/config.auth-static.js +0 -65
- package/templates/mjs/.env.example.auth-static +0 -54
- package/templates/mjs/src/app.auth-static.js +0 -131
- package/templates/mjs/src/cronWorker.js +0 -86
- package/templates/mjs/src/index.js +0 -8
- package/templates/mjs/src/services/database.js +0 -16
- package/templates/mjs/src/swagger.auth-static.js +0 -35
- package/templates/mjs/src/utils/config.auth-static.js +0 -63
- package/templates/ts/.env.example.auth-static +0 -54
- package/templates/ts/src/app.auth-static.ts +0 -131
- package/templates/ts/src/cronWorker.ts +0 -85
- package/templates/ts/src/index.ts +0 -8
- package/templates/ts/src/services/database.ts +0 -16
- package/templates/ts/src/swagger.auth-static.ts +0 -36
- package/templates/ts/src/utils/config.auth-static.ts +0 -68
package/dist/index.js
CHANGED
|
@@ -27,25 +27,20 @@ async function createProject(options) {
|
|
|
27
27
|
await fs.mkdirs(path.join(targetDir, fileName));
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
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;
|
|
30
|
+
// 跳过 auth- 后缀的文件(已合并到统一模板)
|
|
31
|
+
if (/\.auth-(static|jwks)(\.[^.]+)?$/.test(fileName)) {
|
|
32
|
+
continue;
|
|
41
33
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
34
|
+
// 跳过无 auth 后缀的 .env.example(改用条件模板)
|
|
35
|
+
if (fileName === '.env.example' || fileName.endsWith('/.env.example')) {
|
|
36
|
+
continue;
|
|
47
37
|
}
|
|
38
|
+
const outputName = fileName;
|
|
48
39
|
let content = await fs.readFile(srcPath, 'utf-8');
|
|
40
|
+
// 处理条件块:{{#if_eq auth "static"}}...{{else}}...{{/if_eq}}
|
|
41
|
+
content = content.replace(/\{\{#if_eq auth "(static|jwks)"\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if_eq\}\}/g, (_match, value, mainContent, elseContent) => {
|
|
42
|
+
return value === auth ? mainContent : (elseContent || '');
|
|
43
|
+
});
|
|
49
44
|
content = content
|
|
50
45
|
.replace(/\{\{name\}\}/g, name)
|
|
51
46
|
.replace(/\{\{description\}\}/g, description)
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -42,26 +42,28 @@ export async function createProject(options: CreateOptions): Promise<void> {
|
|
|
42
42
|
continue
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
}
|
|
45
|
+
// 跳过 auth- 后缀的文件(已合并到统一模板)
|
|
46
|
+
if (/\.auth-(static|jwks)(\.[^.]+)?$/.test(fileName)) {
|
|
47
|
+
continue
|
|
61
48
|
}
|
|
62
49
|
|
|
50
|
+
// 跳过无 auth 后缀的 .env.example(改用条件模板)
|
|
51
|
+
if (fileName === '.env.example' || fileName.endsWith('/.env.example')) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const outputName = fileName
|
|
56
|
+
|
|
63
57
|
let content = await fs.readFile(srcPath, 'utf-8')
|
|
64
58
|
|
|
59
|
+
// 处理条件块:{{#if_eq auth "static"}}...{{else}}...{{/if_eq}}
|
|
60
|
+
content = content.replace(
|
|
61
|
+
/\{\{#if_eq auth "(static|jwks)"\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if_eq\}\}/g,
|
|
62
|
+
(_match: string, value: string, mainContent: string, elseContent: string) => {
|
|
63
|
+
return value === auth ? mainContent : (elseContent || '')
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
65
67
|
content = content
|
|
66
68
|
.replace(/\{\{name\}\}/g, name)
|
|
67
69
|
.replace(/\{\{description\}\}/g, description)
|
|
@@ -38,6 +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
|
+
{{#if_eq auth "static"}}
|
|
42
|
+
# ---------- Auth: static (HS256 shared secret) ----------
|
|
43
|
+
# [REQUIRED in production] Use a strong random string, e.g. `openssl rand -hex 32`
|
|
44
|
+
JWT_SECRET=please-change-this-in-production
|
|
45
|
+
JWT_EXPIRES_IN=7d
|
|
46
|
+
{{else}}
|
|
41
47
|
# ---------- Auth: jwks (OIDC compatible) ----------
|
|
42
48
|
# Works with any OIDC-compliant IdP. Common issuer formats:
|
|
43
49
|
# Keycloak: https://kc.example.com/realms/myrealm
|
|
@@ -50,6 +56,7 @@ SWAGGER_ENABLED=true
|
|
|
50
56
|
OIDC_ISSUER=
|
|
51
57
|
OIDC_AUDIENCE=
|
|
52
58
|
OIDC_JWKS_URI=
|
|
59
|
+
{{/if_eq}}
|
|
53
60
|
|
|
54
61
|
# ---------- Logging ----------
|
|
55
62
|
# error | warn | info | http | verbose | debug | silly
|
|
@@ -2,7 +2,7 @@ module.exports = {
|
|
|
2
2
|
apps: [
|
|
3
3
|
{
|
|
4
4
|
name: '{{name}}-web',
|
|
5
|
-
script: 'src/
|
|
5
|
+
script: 'src/web-app.js',
|
|
6
6
|
instances: 'max',
|
|
7
7
|
exec_mode: 'cluster',
|
|
8
8
|
env: {
|
|
@@ -16,7 +16,7 @@ module.exports = {
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: '{{name}}-cron',
|
|
19
|
-
script: 'src/
|
|
19
|
+
script: 'src/cron-app.js',
|
|
20
20
|
instances: 1,
|
|
21
21
|
exec_mode: 'fork',
|
|
22
22
|
autorestart: true,
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"restart": "pm2 restart all",
|
|
10
10
|
"delete": "pm2 delete all",
|
|
11
11
|
"logs": "pm2 logs",
|
|
12
|
-
"web:dev": "npx dotenvx run -- node --watch src/
|
|
13
|
-
"cron:dev": "npx dotenvx run -- node --watch src/
|
|
12
|
+
"web:dev": "npx dotenvx run -- node --watch src/web-app.js",
|
|
13
|
+
"cron:dev": "npx dotenvx run -- node --watch src/cron-app.js",
|
|
14
14
|
"seed": "npx dotenvx run -- node src/commands/seed.js",
|
|
15
15
|
"migrate": "npx dotenvx run -- node src/commands/migrate.js",
|
|
16
16
|
"build": "cp -r src dist",
|
|
@@ -22,13 +22,13 @@
|
|
|
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.4",
|
|
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.1.
|
|
31
|
+
"skweb-framework": "^2.1.1",
|
|
32
32
|
"skweb-plugin": "^2.1.1",
|
|
33
33
|
"skweb-util": "^2.0.0",
|
|
34
34
|
"winston": "^3.19.0",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require('dotenv/config')
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const { framework } = require('./cron-framework.js')
|
|
5
|
+
const { logger } = require('./utils/logger.js')
|
|
6
|
+
const { checkRequiredEnv } = require('./utils/config.js')
|
|
7
|
+
|
|
8
|
+
async function startCronWorker() {
|
|
9
|
+
logger.info('[cron-worker] Starting cron worker process...')
|
|
10
|
+
|
|
11
|
+
if (!checkRequiredEnv()) {
|
|
12
|
+
logger.error('[cron-worker] Configuration validation failed, exiting...')
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await framework.setup({
|
|
17
|
+
modelsDir: path.join(__dirname, './models'),
|
|
18
|
+
cronActionsDir: path.join(__dirname, './cronActions')
|
|
19
|
+
})
|
|
20
|
+
await framework.start()
|
|
21
|
+
logger.info('[cron-worker] Cron worker started')
|
|
22
|
+
|
|
23
|
+
process.on('uncaughtException', (err) => {
|
|
24
|
+
logger.error(`[cron-worker] uncaughtException: ${err.stack || err.message}`)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
})
|
|
27
|
+
process.on('unhandledRejection', (reason) => {
|
|
28
|
+
logger.error(`[cron-worker] unhandledRejection: ${reason instanceof Error ? reason.stack : String(reason)}`)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (require.main === module) {
|
|
34
|
+
startCronWorker().catch((err) => {
|
|
35
|
+
logger.error('[cron-worker] Fatal error:', err)
|
|
36
|
+
process.exit(1)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { startCronWorker }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
const { CronFramework } = require('skweb-framework')
|
|
3
|
+
const { logger } = require('./utils/logger.js')
|
|
4
|
+
|
|
5
|
+
const framework = new CronFramework({
|
|
6
|
+
db: {
|
|
7
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
8
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
9
|
+
username: process.env.DB_USER || 'root',
|
|
10
|
+
password: process.env.DB_PASS || '',
|
|
11
|
+
database: process.env.DB_NAME || 'test-project',
|
|
12
|
+
dialect: process.env.DB_DIALECT || 'mysql',
|
|
13
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
14
|
+
underscored: true,
|
|
15
|
+
logging: process.env.DEBUG_DB === 'true'
|
|
16
|
+
},
|
|
17
|
+
redis: {
|
|
18
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
19
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
20
|
+
password: process.env.REDIS_PASSWORD || undefined
|
|
21
|
+
},
|
|
22
|
+
channel: 'cron:task:cmd',
|
|
23
|
+
logger,
|
|
24
|
+
onSaveTask: async (taskData) => {
|
|
25
|
+
const SysCronTask = framework.db?.sequelize?.models.SysCronTask
|
|
26
|
+
if (SysCronTask) {
|
|
27
|
+
await SysCronTask.update(
|
|
28
|
+
{ cache: taskData.cache, data: taskData.data },
|
|
29
|
+
{ where: { id: taskData.taskId } }
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
loadTasks: async () => {
|
|
34
|
+
const SysCronTask = framework.db?.sequelize?.models.SysCronTask
|
|
35
|
+
if (!SysCronTask) return []
|
|
36
|
+
return (await SysCronTask.findAll()).map((t) => ({
|
|
37
|
+
id: String(t.id),
|
|
38
|
+
cronTime: t.cronTime,
|
|
39
|
+
method: t.method
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
module.exports = { framework }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
'use strict'
|
|
2
3
|
|
|
3
4
|
const path = require('path')
|
|
@@ -18,6 +19,13 @@ function buildOpenApiSpec() {
|
|
|
18
19
|
],
|
|
19
20
|
components: {
|
|
20
21
|
securitySchemes: {
|
|
22
|
+
{{#if_eq auth "static"}}
|
|
23
|
+
bearerAuth: {
|
|
24
|
+
type: 'http',
|
|
25
|
+
scheme: 'bearer',
|
|
26
|
+
bearerFormat: 'JWT'
|
|
27
|
+
}
|
|
28
|
+
{{else}}
|
|
21
29
|
oidcAuth: {
|
|
22
30
|
type: 'openIdConnect',
|
|
23
31
|
openIdConnectUrl: process.env.OIDC_JWKS_URI
|
|
@@ -26,6 +34,7 @@ function buildOpenApiSpec() {
|
|
|
26
34
|
? `${process.env.OIDC_ISSUER.replace(/\/$/, '')}/.well-known/openid-configuration`
|
|
27
35
|
: undefined)
|
|
28
36
|
}
|
|
37
|
+
{{/if_eq}}
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
const { logger } = require('./logger.js')
|
|
2
3
|
|
|
3
4
|
function validateConfig() {
|
|
@@ -38,6 +39,18 @@ function validateConfig() {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
{{#if_eq auth "static"}}
|
|
43
|
+
function checkJwtSecret() {
|
|
44
|
+
if (process.env.JWT_SECRET === 'please-change-this-in-production' || !process.env.JWT_SECRET) {
|
|
45
|
+
if (process.env.NODE_ENV === 'production') {
|
|
46
|
+
logger.error('[config] JWT_SECRET must be set in production!')
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
logger.warn('[config] WARNING: JWT_SECRET is using default value, please change in production!')
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
{{else}}
|
|
41
54
|
function checkOIDCConfig() {
|
|
42
55
|
const issuer = process.env.OIDC_ISSUER
|
|
43
56
|
const jwksUri = process.env.OIDC_JWKS_URI
|
|
@@ -50,6 +63,7 @@ function checkOIDCConfig() {
|
|
|
50
63
|
}
|
|
51
64
|
return true
|
|
52
65
|
}
|
|
66
|
+
{{/if_eq}}
|
|
53
67
|
|
|
54
68
|
function checkRequiredEnv() {
|
|
55
69
|
const result = validateConfig()
|
|
@@ -64,4 +78,8 @@ function checkRequiredEnv() {
|
|
|
64
78
|
return true
|
|
65
79
|
}
|
|
66
80
|
|
|
81
|
+
{{#if_eq auth "static"}}
|
|
82
|
+
module.exports = { validateConfig, checkJwtSecret, checkRequiredEnv }
|
|
83
|
+
{{else}}
|
|
67
84
|
module.exports = { validateConfig, checkOIDCConfig, checkRequiredEnv }
|
|
85
|
+
{{/if_eq}}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
require('dotenv/config')
|
|
2
3
|
|
|
3
4
|
const path = require('path')
|
|
@@ -5,11 +6,25 @@ const express = require('skweb-express').default
|
|
|
5
6
|
const expressLogger = require('skweb-express-logger').default
|
|
6
7
|
const expressErrorHandler = require('skweb-express-logger').errorHandler
|
|
7
8
|
const swaggerUi = require('swagger-ui-express')
|
|
8
|
-
const {
|
|
9
|
+
const { framework } = require('./web-framework.js')
|
|
9
10
|
const { logger } = require('./utils/logger.js')
|
|
10
|
-
const { checkRequiredEnv, checkOIDCConfig } = require('./utils/config.js')
|
|
11
|
+
const { checkRequiredEnv{{#if_eq auth "static"}}, checkJwtSecret{{/if_eq}}{{#if_eq auth "jwks"}}, checkOIDCConfig{{/if_eq}} } = require('./utils/config.js')
|
|
11
12
|
const { buildOpenApiSpec } = require('./swagger.js')
|
|
12
13
|
|
|
14
|
+
{{#if_eq auth "static"}}
|
|
15
|
+
/**
|
|
16
|
+
* JWT 配置(static 模式,HS256 共享密钥)。
|
|
17
|
+
*/
|
|
18
|
+
function buildJwtOptions() {
|
|
19
|
+
return {
|
|
20
|
+
provider: 'static',
|
|
21
|
+
secret: process.env.JWT_SECRET || 'please-change-this-in-production',
|
|
22
|
+
algorithms: ['HS256'],
|
|
23
|
+
sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
|
|
24
|
+
requestProperty: 'resolvedToken'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
{{else}}
|
|
13
28
|
/**
|
|
14
29
|
* JWT 配置(jwks 模式,通过 OIDC 兼容的 JWKS 端点验证 RS256 签名)。
|
|
15
30
|
*/
|
|
@@ -28,7 +43,12 @@ function buildJwtOptions() {
|
|
|
28
43
|
requestProperty: 'resolvedToken'
|
|
29
44
|
}
|
|
30
45
|
}
|
|
46
|
+
{{/if_eq}}
|
|
31
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Swagger UI 仅在 NODE_ENV 为 development / test,且 SWAGGER_ENABLED !== 'false' 时启用。
|
|
50
|
+
* 生产环境(production / unset)默认禁用,避免暴露内部 API 细节。
|
|
51
|
+
*/
|
|
32
52
|
function shouldEnableSwagger() {
|
|
33
53
|
const env = (process.env.NODE_ENV || '').toLowerCase()
|
|
34
54
|
if (env !== 'development' && env !== 'test') return false
|
|
@@ -43,29 +63,18 @@ async function bootstrap() {
|
|
|
43
63
|
logger.error('[app] Configuration validation failed, exiting...')
|
|
44
64
|
process.exit(1)
|
|
45
65
|
}
|
|
66
|
+
{{#if_eq auth "static"}}
|
|
67
|
+
if (!checkJwtSecret()) {
|
|
68
|
+
logger.error('[app] JWT configuration validation failed, exiting...')
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
{{else}}
|
|
46
72
|
if (!checkOIDCConfig()) {
|
|
47
73
|
logger.error('[app] OIDC configuration validation failed, exiting...')
|
|
48
74
|
process.exit(1)
|
|
49
75
|
}
|
|
76
|
+
{{/if_eq}}
|
|
50
77
|
|
|
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
78
|
const app = framework.createApp()
|
|
70
79
|
|
|
71
80
|
await framework.setup({
|
|
@@ -86,7 +95,11 @@ async function bootstrap() {
|
|
|
86
95
|
app.setJWT('default', jwtOptions)
|
|
87
96
|
app.setJwtHintHeader('JwtHint')
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
{{#if_eq auth "static"}}
|
|
99
|
+
logger.info('[app] JWT auth provider: static (HS256)')
|
|
100
|
+
{{else}}
|
|
101
|
+
logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks?.issuer || jwtOptions.jwks?.jwksUri})`)
|
|
102
|
+
{{/if_eq}}
|
|
90
103
|
|
|
91
104
|
app.use(expressLogger())
|
|
92
105
|
app.use(express.static(path.join(__dirname, '..', 'public')))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
const { WebFramework } = require('skweb-framework')
|
|
3
|
+
|
|
4
|
+
const framework = new WebFramework({
|
|
5
|
+
db: {
|
|
6
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
7
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
8
|
+
username: process.env.DB_USER || 'root',
|
|
9
|
+
password: process.env.DB_PASS || '',
|
|
10
|
+
database: process.env.DB_NAME || 'test-project',
|
|
11
|
+
dialect: process.env.DB_DIALECT || 'mysql',
|
|
12
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
13
|
+
underscored: true,
|
|
14
|
+
logging: process.env.DEBUG_DB === 'true'
|
|
15
|
+
},
|
|
16
|
+
redis: {
|
|
17
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
18
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
19
|
+
password: process.env.REDIS_PASSWORD || undefined
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
module.exports = { framework }
|
|
@@ -38,6 +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
|
+
{{#if_eq auth "static"}}
|
|
42
|
+
# ---------- Auth: static (HS256 shared secret) ----------
|
|
43
|
+
# [REQUIRED in production] Use a strong random string, e.g. `openssl rand -hex 32`
|
|
44
|
+
JWT_SECRET=please-change-this-in-production
|
|
45
|
+
JWT_EXPIRES_IN=7d
|
|
46
|
+
{{else}}
|
|
41
47
|
# ---------- Auth: jwks (OIDC compatible) ----------
|
|
42
48
|
# Works with any OIDC-compliant IdP. Common issuer formats:
|
|
43
49
|
# Keycloak: https://kc.example.com/realms/myrealm
|
|
@@ -50,6 +56,7 @@ SWAGGER_ENABLED=true
|
|
|
50
56
|
OIDC_ISSUER=
|
|
51
57
|
OIDC_AUDIENCE=
|
|
52
58
|
OIDC_JWKS_URI=
|
|
59
|
+
{{/if_eq}}
|
|
53
60
|
|
|
54
61
|
# ---------- Logging ----------
|
|
55
62
|
# error | warn | info | http | verbose | debug | silly
|
|
@@ -2,7 +2,7 @@ module.exports = {
|
|
|
2
2
|
apps: [
|
|
3
3
|
{
|
|
4
4
|
name: '{{name}}-web',
|
|
5
|
-
script: 'src/
|
|
5
|
+
script: 'src/web-app.js',
|
|
6
6
|
instances: 'max',
|
|
7
7
|
exec_mode: 'cluster',
|
|
8
8
|
env: {
|
|
@@ -16,7 +16,7 @@ module.exports = {
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: '{{name}}-cron',
|
|
19
|
-
script: 'src/
|
|
19
|
+
script: 'src/cron-app.js',
|
|
20
20
|
instances: 1,
|
|
21
21
|
exec_mode: 'fork',
|
|
22
22
|
autorestart: true,
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"restart": "pm2 restart all",
|
|
10
10
|
"delete": "pm2 delete all",
|
|
11
11
|
"logs": "pm2 logs",
|
|
12
|
-
"web:dev": "npx dotenvx run -- node --watch src/
|
|
13
|
-
"cron:dev": "npx dotenvx run -- node --watch src/
|
|
12
|
+
"web:dev": "npx dotenvx run -- node --watch src/web-app.js",
|
|
13
|
+
"cron:dev": "npx dotenvx run -- node --watch src/cron-app.js",
|
|
14
14
|
"seed": "npx dotenvx run -- node src/commands/seed.js",
|
|
15
15
|
"migrate": "npx dotenvx run -- node src/commands/migrate.js",
|
|
16
16
|
"build": "cp -r src dist",
|
|
@@ -22,13 +22,13 @@
|
|
|
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.4",
|
|
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.1.
|
|
31
|
+
"skweb-framework": "^2.1.1",
|
|
32
32
|
"skweb-plugin": "^2.1.1",
|
|
33
33
|
"skweb-util": "^2.0.0",
|
|
34
34
|
"winston": "^3.19.0",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { framework } from './cron-framework.js'
|
|
6
|
+
import logger from './utils/logger.js'
|
|
7
|
+
import { checkRequiredEnv } from './utils/config.js'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
async function startCronWorker() {
|
|
13
|
+
logger.info('[cron-worker] Starting cron worker process...')
|
|
14
|
+
|
|
15
|
+
if (!checkRequiredEnv()) {
|
|
16
|
+
logger.error('[cron-worker] Configuration validation failed, exiting...')
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await framework.setup({
|
|
21
|
+
modelsDir: path.join(__dirname, './models'),
|
|
22
|
+
cronActionsDir: path.join(__dirname, './cronActions')
|
|
23
|
+
})
|
|
24
|
+
await framework.start()
|
|
25
|
+
logger.info('[cron-worker] Cron worker started')
|
|
26
|
+
|
|
27
|
+
process.on('uncaughtException', (err) => {
|
|
28
|
+
logger.error(`[cron-worker] uncaughtException: ${err.stack || err.message}`)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
})
|
|
31
|
+
process.on('unhandledRejection', (reason) => {
|
|
32
|
+
logger.error(`[cron-worker] unhandledRejection: ${reason instanceof Error ? reason.stack : String(reason)}`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`
|
|
38
|
+
if (isMainModule) {
|
|
39
|
+
startCronWorker().catch((err) => {
|
|
40
|
+
logger.error('[cron-worker] Fatal error:', err)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { startCronWorker }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { CronFramework } from 'skweb-framework'
|
|
3
|
+
import logger from './utils/logger.js'
|
|
4
|
+
|
|
5
|
+
const framework = new CronFramework({
|
|
6
|
+
db: {
|
|
7
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
8
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
9
|
+
username: process.env.DB_USER || 'root',
|
|
10
|
+
password: process.env.DB_PASS || '',
|
|
11
|
+
database: process.env.DB_NAME || 'test-project',
|
|
12
|
+
dialect: process.env.DB_DIALECT || 'mysql',
|
|
13
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
14
|
+
underscored: true,
|
|
15
|
+
logging: process.env.DEBUG_DB === 'true'
|
|
16
|
+
},
|
|
17
|
+
redis: {
|
|
18
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
19
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
20
|
+
password: process.env.REDIS_PASSWORD || undefined
|
|
21
|
+
},
|
|
22
|
+
channel: 'cron:task:cmd',
|
|
23
|
+
logger,
|
|
24
|
+
onSaveTask: async (taskData) => {
|
|
25
|
+
const SysCronTask = framework.db?.sequelize?.models.SysCronTask
|
|
26
|
+
if (SysCronTask) {
|
|
27
|
+
await SysCronTask.update(
|
|
28
|
+
{ cache: taskData.cache, data: taskData.data },
|
|
29
|
+
{ where: { id: taskData.taskId } }
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
loadTasks: async () => {
|
|
34
|
+
const SysCronTask = framework.db?.sequelize?.models.SysCronTask
|
|
35
|
+
if (!SysCronTask) return []
|
|
36
|
+
return (await SysCronTask.findAll()).map((t) => ({
|
|
37
|
+
id: String(t.id),
|
|
38
|
+
cronTime: t.cronTime,
|
|
39
|
+
method: t.method
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export { framework }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
import path from 'path'
|
|
2
3
|
import { fileURLToPath } from 'url'
|
|
3
4
|
import swaggerJSDoc from 'swagger-jsdoc'
|
|
@@ -20,6 +21,13 @@ export function buildOpenApiSpec() {
|
|
|
20
21
|
],
|
|
21
22
|
components: {
|
|
22
23
|
securitySchemes: {
|
|
24
|
+
{{#if_eq auth "static"}}
|
|
25
|
+
bearerAuth: {
|
|
26
|
+
type: 'http',
|
|
27
|
+
scheme: 'bearer',
|
|
28
|
+
bearerFormat: 'JWT'
|
|
29
|
+
}
|
|
30
|
+
{{else}}
|
|
23
31
|
oidcAuth: {
|
|
24
32
|
type: 'openIdConnect',
|
|
25
33
|
openIdConnectUrl: process.env.OIDC_JWKS_URI
|
|
@@ -28,6 +36,7 @@ export function buildOpenApiSpec() {
|
|
|
28
36
|
? `${process.env.OIDC_ISSUER.replace(/\/$/, '')}/.well-known/openid-configuration`
|
|
29
37
|
: undefined)
|
|
30
38
|
}
|
|
39
|
+
{{/if_eq}}
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
42
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
import logger from './logger.js'
|
|
2
3
|
|
|
3
4
|
export function validateConfig() {
|
|
@@ -38,6 +39,18 @@ export function validateConfig() {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
{{#if_eq auth "static"}}
|
|
43
|
+
export function checkJwtSecret() {
|
|
44
|
+
if (process.env.JWT_SECRET === 'please-change-this-in-production' || !process.env.JWT_SECRET) {
|
|
45
|
+
if (process.env.NODE_ENV === 'production') {
|
|
46
|
+
logger.error('[config] JWT_SECRET must be set in production!')
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
logger.warn('[config] WARNING: JWT_SECRET is using default value, please change in production!')
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
{{else}}
|
|
41
54
|
export function checkOIDCConfig() {
|
|
42
55
|
const issuer = process.env.OIDC_ISSUER
|
|
43
56
|
const jwksUri = process.env.OIDC_JWKS_URI
|
|
@@ -50,6 +63,7 @@ export function checkOIDCConfig() {
|
|
|
50
63
|
}
|
|
51
64
|
return true
|
|
52
65
|
}
|
|
66
|
+
{{/if_eq}}
|
|
53
67
|
|
|
54
68
|
export function checkRequiredEnv() {
|
|
55
69
|
const result = validateConfig()
|