create-skweb 3.4.0 → 3.5.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 +17 -16
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/templates/cjs/.env.example +7 -1
- package/templates/cjs/package.json +2 -0
- package/templates/cjs/scripts/get-token.js +58 -0
- package/templates/cjs/src/controllers/user.js +0 -25
- package/templates/cjs/src/swagger.js +2 -34
- package/templates/mjs/.env.example +8 -2
- package/templates/mjs/package.json +2 -0
- package/templates/mjs/scripts/get-token.js +55 -0
- package/templates/mjs/src/controllers/user.js +0 -25
- package/templates/mjs/src/swagger.js +2 -34
- package/templates/ts/.env.example +7 -1
- package/templates/ts/package.json +2 -0
- package/templates/ts/scripts/get-token.ts +55 -0
- package/templates/ts/src/controllers/user.ts +0 -25
- package/templates/ts/src/swagger.ts +2 -34
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
> create-skweb@3.
|
|
3
|
-
> tsc --outDir dist --rootDir src
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
> create-skweb@3.
|
|
7
|
-
> rollup -c rollup.config.mjs && node scripts/inject-shebang.mjs && chmod +x dist/cli.js
|
|
8
|
-
|
|
9
|
-
[36m
|
|
10
|
-
[1msrc/index.ts[22m → [1mdist/index.js[22m...[39m
|
|
11
|
-
[1m[33m(!) [plugin typescript] @rollup/plugin-typescript: outputToFilesystem option is defaulting to true.[39m[22m
|
|
12
|
-
[32mcreated [1mdist/index.js[22m in [
|
|
13
|
-
[36m
|
|
14
|
-
[1msrc/cli.ts[22m → [1mdist/cli.js[22m...[39m
|
|
15
|
-
[32mcreated [1mdist/cli.js[22m in [
|
|
16
|
-
[inject-shebang] Shebang added to /Users/stayknight/projects/skweb-mono/tools/create-skweb/dist/cli.js
|
|
1
|
+
|
|
2
|
+
> create-skweb@3.5.0 prebuild
|
|
3
|
+
> tsc --outDir dist --rootDir src
|
|
4
|
+
|
|
5
|
+
⠙[1G[0K
|
|
6
|
+
> create-skweb@3.5.0 build
|
|
7
|
+
> rollup -c rollup.config.mjs && node scripts/inject-shebang.mjs && chmod +x dist/cli.js
|
|
8
|
+
|
|
9
|
+
⠙[1G[0K[36m
|
|
10
|
+
[1msrc/index.ts[22m → [1mdist/index.js[22m...[39m
|
|
11
|
+
[1m[33m(!) [plugin typescript] @rollup/plugin-typescript: outputToFilesystem option is defaulting to true.[39m[22m
|
|
12
|
+
[32mcreated [1mdist/index.js[22m in [1m376ms[22m[39m
|
|
13
|
+
[36m
|
|
14
|
+
[1msrc/cli.ts[22m → [1mdist/cli.js[22m...[39m
|
|
15
|
+
[32mcreated [1mdist/cli.js[22m in [1m239ms[22m[39m
|
|
16
|
+
[inject-shebang] Shebang added to /Users/stayknight/projects/skweb-mono/tools/create-skweb/dist/cli.js
|
|
17
|
+
⠙[1G[0K
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# create-skweb
|
|
2
2
|
|
|
3
|
+
## 3.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a43c261: feat: 简化模板 auth/swagger 配置,添加 get-token 脚本
|
|
8
|
+
|
|
9
|
+
- Swagger 统一使用 bearerAuth (http + bearer + JWT),不再使用 oauth2 流程
|
|
10
|
+
- 移除 clientSecret 相关配置
|
|
11
|
+
- 添加 scripts/get-token.{js,ts}:从 Keycloak (ROPC grant) 获取 access token
|
|
12
|
+
- package.json 添加 get-token 脚本和 axios 依赖
|
|
13
|
+
- .env.example 添加 OIDC_CLIENT_ID/OIDC_USERNAME/OIDC_PASSWORD (供 get-token 使用)
|
|
14
|
+
- 所有三个模板(ts/mjs/cjs)已同步
|
|
15
|
+
|
|
3
16
|
## 3.3.9
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -44,7 +44,7 @@ SWAGGER_ENABLED=true
|
|
|
44
44
|
JWT_SECRET=please-change-this-in-production
|
|
45
45
|
JWT_EXPIRES_IN=7d
|
|
46
46
|
{{else}}
|
|
47
|
-
# ---------- Auth: jwks (OIDC compatible) ----------
|
|
47
|
+
# ---------- Auth: jwks (OIDC compatible, public-key signature) ----------
|
|
48
48
|
# Works with any OIDC-compliant IdP. Common issuer formats:
|
|
49
49
|
# Keycloak: https://kc.example.com/realms/myrealm
|
|
50
50
|
# Auth0: https://your-tenant.auth0.com/
|
|
@@ -53,9 +53,15 @@ JWT_EXPIRES_IN=7d
|
|
|
53
53
|
# `audience` is the client id of this API in the IdP (recommended).
|
|
54
54
|
# JWKS URI is auto-derived as ${OIDC_ISSUER}/.well-known/jwks.json
|
|
55
55
|
# unless OIDC_JWKS_URI is set explicitly.
|
|
56
|
+
# This API only verifies the JWT signature; it does not handle the OAuth flow.
|
|
57
|
+
# Use `npm run get-token` to obtain a token for local testing.
|
|
56
58
|
OIDC_ISSUER=
|
|
57
59
|
OIDC_AUDIENCE=
|
|
58
60
|
OIDC_JWKS_URI=
|
|
61
|
+
# The following are read by scripts/get-token.js to fetch a test token:
|
|
62
|
+
OIDC_CLIENT_ID=
|
|
63
|
+
OIDC_USERNAME=
|
|
64
|
+
OIDC_PASSWORD=
|
|
59
65
|
{{/if_eq}}
|
|
60
66
|
|
|
61
67
|
# ---------- Logging ----------
|
|
@@ -13,6 +13,7 @@
|
|
|
13
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
|
+
"get-token": "node scripts/get-token.js",
|
|
16
17
|
"build": "cp -r src dist",
|
|
17
18
|
"lint": "eslint --fix --ext .js src",
|
|
18
19
|
"test": "npx dotenvx run -- mocha test/**/*.js",
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@dotenvx/dotenvx": "^1.75.1",
|
|
25
|
+
"axios": "^1.7.7",
|
|
24
26
|
"http-errors": "^2.0.1",
|
|
25
27
|
"skweb-express": "^2.1.0",
|
|
26
28
|
"skweb-express-jwt": "^3.0.0",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get-token.js
|
|
3
|
+
* -----------------------------------------------------------------------------
|
|
4
|
+
* Fetch a Keycloak access token using Resource Owner Password Credentials grant.
|
|
5
|
+
* Reads configuration from the project .env (via dotenv) and prints the token
|
|
6
|
+
* to stdout so you can pipe it into curl / paste it into Swagger UI.
|
|
7
|
+
*
|
|
8
|
+
* Required env vars in .env:
|
|
9
|
+
* OIDC_ISSUER - e.g. https://kc.example.com/realms/myrealm
|
|
10
|
+
* OIDC_CLIENT_ID - the public client id (must enable "Direct access grants")
|
|
11
|
+
* OIDC_USERNAME - the test user
|
|
12
|
+
* OIDC_PASSWORD - the test user's password
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/get-token.js
|
|
16
|
+
* TOKEN=$(node scripts/get-token.js)
|
|
17
|
+
* curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/users
|
|
18
|
+
*/
|
|
19
|
+
'use strict'
|
|
20
|
+
|
|
21
|
+
require('@dotenvx/dotenvx').config()
|
|
22
|
+
|
|
23
|
+
const axios = require('axios')
|
|
24
|
+
|
|
25
|
+
async function getToken() {
|
|
26
|
+
const issuer = (process.env.OIDC_ISSUER || '').replace(/\/$/, '')
|
|
27
|
+
const clientId = process.env.OIDC_CLIENT_ID
|
|
28
|
+
const username = process.env.OIDC_USERNAME
|
|
29
|
+
const password = process.env.OIDC_PASSWORD
|
|
30
|
+
|
|
31
|
+
if (!issuer) throw new Error('OIDC_ISSUER is missing in .env')
|
|
32
|
+
if (!clientId) throw new Error('OIDC_CLIENT_ID is missing in .env')
|
|
33
|
+
if (!username) throw new Error('OIDC_USERNAME is missing in .env')
|
|
34
|
+
if (!password) throw new Error('OIDC_PASSWORD is missing in .env')
|
|
35
|
+
|
|
36
|
+
const params = new URLSearchParams({
|
|
37
|
+
grant_type: 'password',
|
|
38
|
+
client_id: clientId,
|
|
39
|
+
username,
|
|
40
|
+
password
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const tokenUrl = `${issuer}/protocol/openid-connect/token`
|
|
44
|
+
const response = await axios.post(tokenUrl, params.toString(), {
|
|
45
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return response.data.access_token
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getToken()
|
|
52
|
+
.then((token) => {
|
|
53
|
+
process.stdout.write(token + '\n')
|
|
54
|
+
})
|
|
55
|
+
.catch((err) => {
|
|
56
|
+
console.error('❌ Failed to fetch token:', err.response?.data || err.message)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
})
|
|
@@ -7,13 +7,8 @@ const { SysUser } = require('../models/SysUser.js')
|
|
|
7
7
|
* get:
|
|
8
8
|
* summary: 获取用户列表
|
|
9
9
|
* tags: [Users]
|
|
10
|
-
* {{#if_eq auth "static"}}
|
|
11
10
|
* security:
|
|
12
11
|
* - bearerAuth: []
|
|
13
|
-
* {{else}}
|
|
14
|
-
* security:
|
|
15
|
-
* - oidcAuth: []
|
|
16
|
-
* {{/if_eq}}
|
|
17
12
|
* parameters:
|
|
18
13
|
* - in: query
|
|
19
14
|
* name: page
|
|
@@ -50,13 +45,8 @@ const userList = {
|
|
|
50
45
|
* get:
|
|
51
46
|
* summary: 获取单个用户
|
|
52
47
|
* tags: [Users]
|
|
53
|
-
* {{#if_eq auth "static"}}
|
|
54
48
|
* security:
|
|
55
49
|
* - bearerAuth: []
|
|
56
|
-
* {{else}}
|
|
57
|
-
* security:
|
|
58
|
-
* - oidcAuth: []
|
|
59
|
-
* {{/if_eq}}
|
|
60
50
|
* parameters:
|
|
61
51
|
* - in: path
|
|
62
52
|
* name: id
|
|
@@ -93,13 +83,8 @@ const userGet = {
|
|
|
93
83
|
* post:
|
|
94
84
|
* summary: 创建用户
|
|
95
85
|
* tags: [Users]
|
|
96
|
-
* {{#if_eq auth "static"}}
|
|
97
86
|
* security:
|
|
98
87
|
* - bearerAuth: []
|
|
99
|
-
* {{else}}
|
|
100
|
-
* security:
|
|
101
|
-
* - oidcAuth: []
|
|
102
|
-
* {{/if_eq}}
|
|
103
88
|
* requestBody:
|
|
104
89
|
* required: true
|
|
105
90
|
* content:
|
|
@@ -141,13 +126,8 @@ const userCreate = {
|
|
|
141
126
|
* put:
|
|
142
127
|
* summary: 更新用户
|
|
143
128
|
* tags: [Users]
|
|
144
|
-
* {{#if_eq auth "static"}}
|
|
145
129
|
* security:
|
|
146
130
|
* - bearerAuth: []
|
|
147
|
-
* {{else}}
|
|
148
|
-
* security:
|
|
149
|
-
* - oidcAuth: []
|
|
150
|
-
* {{/if_eq}}
|
|
151
131
|
* parameters:
|
|
152
132
|
* - in: path
|
|
153
133
|
* name: id
|
|
@@ -205,13 +185,8 @@ const userUpdate = {
|
|
|
205
185
|
* delete:
|
|
206
186
|
* summary: 删除用户
|
|
207
187
|
* tags: [Users]
|
|
208
|
-
* {{#if_eq auth "static"}}
|
|
209
188
|
* security:
|
|
210
189
|
* - bearerAuth: []
|
|
211
|
-
* {{else}}
|
|
212
|
-
* security:
|
|
213
|
-
* - oidcAuth: []
|
|
214
|
-
* {{/if_eq}}
|
|
215
190
|
* parameters:
|
|
216
191
|
* - in: path
|
|
217
192
|
* name: id
|
|
@@ -19,44 +19,12 @@ function buildOpenApiSpec() {
|
|
|
19
19
|
],
|
|
20
20
|
components: {
|
|
21
21
|
securitySchemes: {
|
|
22
|
-
{{#if_eq auth "static"}}
|
|
23
22
|
bearerAuth: {
|
|
24
23
|
type: 'http',
|
|
25
24
|
scheme: 'bearer',
|
|
26
|
-
bearerFormat: 'JWT'
|
|
25
|
+
bearerFormat: 'JWT',
|
|
26
|
+
description: 'Paste your JWT access token (use `npm run get-token` to fetch one).'
|
|
27
27
|
}
|
|
28
|
-
{{else}}
|
|
29
|
-
oidcAuth: (() => {
|
|
30
|
-
// Resolve OIDC endpoints from OIDC_ISSUER or OIDC_JWKS_URI
|
|
31
|
-
// Supports Keycloak realms and generic OIDC providers
|
|
32
|
-
const issuer = process.env.OIDC_ISSUER && process.env.OIDC_ISSUER.replace(/\/$/, '')
|
|
33
|
-
const jwksUri = process.env.OIDC_JWKS_URI
|
|
34
|
-
let baseUrl
|
|
35
|
-
if (issuer) {
|
|
36
|
-
baseUrl = issuer
|
|
37
|
-
} else if (jwksUri) {
|
|
38
|
-
// Keycloak JWKS URI: https://host/realms/<name>/protocol/openid-connect/certs
|
|
39
|
-
const match = jwksUri.match(/^(https?:\/\/[^/]+\/realms\/[^/]+)/)
|
|
40
|
-
if (match) baseUrl = match[1]
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
type: 'oauth2',
|
|
44
|
-
flows: {
|
|
45
|
-
authorizationCode: {
|
|
46
|
-
authorizationUrl: baseUrl ? `${baseUrl}/protocol/openid-connect/auth` : undefined,
|
|
47
|
-
tokenUrl: baseUrl ? `${baseUrl}/protocol/openid-connect/token` : undefined,
|
|
48
|
-
refreshUrl: baseUrl ? `${baseUrl}/protocol/openid-connect/token` : undefined,
|
|
49
|
-
scopes: {
|
|
50
|
-
openid: 'OpenID Connect scope',
|
|
51
|
-
profile: 'User profile',
|
|
52
|
-
email: 'Email address',
|
|
53
|
-
offline_access: 'Offline token refresh'
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
})()
|
|
59
|
-
{{/if_eq}}
|
|
60
28
|
}
|
|
61
29
|
}
|
|
62
30
|
},
|
|
@@ -44,18 +44,24 @@ SWAGGER_ENABLED=true
|
|
|
44
44
|
JWT_SECRET=please-change-this-in-production
|
|
45
45
|
JWT_EXPIRES_IN=7d
|
|
46
46
|
{{else}}
|
|
47
|
-
# ---------- Auth: jwks (OIDC compatible) ----------
|
|
47
|
+
# ---------- Auth: jwks (OIDC compatible, public-key signature) ----------
|
|
48
48
|
# Works with any OIDC-compliant IdP. Common issuer formats:
|
|
49
49
|
# Keycloak: https://kc.example.com/realms/myrealm
|
|
50
50
|
# Auth0: https://your-tenant.auth0.com/
|
|
51
|
-
# Okta:
|
|
51
|
+
# Okta: https://your-domain.okta.com/oauth2/default
|
|
52
52
|
# Authing: https://your-app.authing.cn/oauth2/default
|
|
53
53
|
# `audience` is the client id of this API in the IdP (recommended).
|
|
54
54
|
# JWKS URI is auto-derived as ${OIDC_ISSUER}/.well-known/jwks.json
|
|
55
55
|
# unless OIDC_JWKS_URI is set explicitly.
|
|
56
|
+
# This API only verifies the JWT signature; it does not handle the OAuth flow.
|
|
57
|
+
# Use `npm run get-token` to obtain a token for local testing.
|
|
56
58
|
OIDC_ISSUER=
|
|
57
59
|
OIDC_AUDIENCE=
|
|
58
60
|
OIDC_JWKS_URI=
|
|
61
|
+
# The following are read by scripts/get-token.js to fetch a test token:
|
|
62
|
+
OIDC_CLIENT_ID=
|
|
63
|
+
OIDC_USERNAME=
|
|
64
|
+
OIDC_PASSWORD=
|
|
59
65
|
{{/if_eq}}
|
|
60
66
|
|
|
61
67
|
# ---------- Logging ----------
|
|
@@ -13,6 +13,7 @@
|
|
|
13
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
|
+
"get-token": "node scripts/get-token.js",
|
|
16
17
|
"build": "cp -r src dist",
|
|
17
18
|
"lint": "eslint --fix --ext .js src",
|
|
18
19
|
"test": "npx dotenvx run -- mocha test/**/*.mjs",
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@dotenvx/dotenvx": "^1.75.1",
|
|
25
|
+
"axios": "^1.7.7",
|
|
24
26
|
"http-errors": "^2.0.1",
|
|
25
27
|
"skweb-express": "^2.1.0",
|
|
26
28
|
"skweb-express-jwt": "^3.0.0",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get-token.js
|
|
3
|
+
* -----------------------------------------------------------------------------
|
|
4
|
+
* Fetch a Keycloak access token using Resource Owner Password Credentials grant.
|
|
5
|
+
* Reads configuration from the project .env (via dotenv) and prints the token
|
|
6
|
+
* to stdout so you can pipe it into curl / paste it into Swagger UI.
|
|
7
|
+
*
|
|
8
|
+
* Required env vars in .env:
|
|
9
|
+
* OIDC_ISSUER - e.g. https://kc.example.com/realms/myrealm
|
|
10
|
+
* OIDC_CLIENT_ID - the public client id (must enable "Direct access grants")
|
|
11
|
+
* OIDC_USERNAME - the test user
|
|
12
|
+
* OIDC_PASSWORD - the test user's password
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/get-token.js
|
|
16
|
+
* TOKEN=$(node scripts/get-token.js)
|
|
17
|
+
* curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/users
|
|
18
|
+
*/
|
|
19
|
+
import '@dotenvx/dotenvx/config.js'
|
|
20
|
+
import axios from 'axios'
|
|
21
|
+
|
|
22
|
+
async function getToken() {
|
|
23
|
+
const issuer = (process.env.OIDC_ISSUER || '').replace(/\/$/, '')
|
|
24
|
+
const clientId = process.env.OIDC_CLIENT_ID
|
|
25
|
+
const username = process.env.OIDC_USERNAME
|
|
26
|
+
const password = process.env.OIDC_PASSWORD
|
|
27
|
+
|
|
28
|
+
if (!issuer) throw new Error('OIDC_ISSUER is missing in .env')
|
|
29
|
+
if (!clientId) throw new Error('OIDC_CLIENT_ID is missing in .env')
|
|
30
|
+
if (!username) throw new Error('OIDC_USERNAME is missing in .env')
|
|
31
|
+
if (!password) throw new Error('OIDC_PASSWORD is missing in .env')
|
|
32
|
+
|
|
33
|
+
const params = new URLSearchParams({
|
|
34
|
+
grant_type: 'password',
|
|
35
|
+
client_id: clientId,
|
|
36
|
+
username,
|
|
37
|
+
password
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const tokenUrl = `${issuer}/protocol/openid-connect/token`
|
|
41
|
+
const response = await axios.post(tokenUrl, params.toString(), {
|
|
42
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return response.data.access_token
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getToken()
|
|
49
|
+
.then((token) => {
|
|
50
|
+
process.stdout.write(token + '\n')
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
console.error('❌ Failed to fetch token:', err.response?.data || err.message)
|
|
54
|
+
process.exit(1)
|
|
55
|
+
})
|
|
@@ -7,13 +7,8 @@ import { SysUser } from '../models/SysUser.js'
|
|
|
7
7
|
* get:
|
|
8
8
|
* summary: 获取用户列表
|
|
9
9
|
* tags: [Users]
|
|
10
|
-
* {{#if_eq auth "static"}}
|
|
11
10
|
* security:
|
|
12
11
|
* - bearerAuth: []
|
|
13
|
-
* {{else}}
|
|
14
|
-
* security:
|
|
15
|
-
* - oidcAuth: []
|
|
16
|
-
* {{/if_eq}}
|
|
17
12
|
* parameters:
|
|
18
13
|
* - in: query
|
|
19
14
|
* name: page
|
|
@@ -50,13 +45,8 @@ const userList = {
|
|
|
50
45
|
* get:
|
|
51
46
|
* summary: 获取单个用户
|
|
52
47
|
* tags: [Users]
|
|
53
|
-
* {{#if_eq auth "static"}}
|
|
54
48
|
* security:
|
|
55
49
|
* - bearerAuth: []
|
|
56
|
-
* {{else}}
|
|
57
|
-
* security:
|
|
58
|
-
* - oidcAuth: []
|
|
59
|
-
* {{/if_eq}}
|
|
60
50
|
* parameters:
|
|
61
51
|
* - in: path
|
|
62
52
|
* name: id
|
|
@@ -93,13 +83,8 @@ const userGet = {
|
|
|
93
83
|
* post:
|
|
94
84
|
* summary: 创建用户
|
|
95
85
|
* tags: [Users]
|
|
96
|
-
* {{#if_eq auth "static"}}
|
|
97
86
|
* security:
|
|
98
87
|
* - bearerAuth: []
|
|
99
|
-
* {{else}}
|
|
100
|
-
* security:
|
|
101
|
-
* - oidcAuth: []
|
|
102
|
-
* {{/if_eq}}
|
|
103
88
|
* requestBody:
|
|
104
89
|
* required: true
|
|
105
90
|
* content:
|
|
@@ -141,13 +126,8 @@ const userCreate = {
|
|
|
141
126
|
* put:
|
|
142
127
|
* summary: 更新用户
|
|
143
128
|
* tags: [Users]
|
|
144
|
-
* {{#if_eq auth "static"}}
|
|
145
129
|
* security:
|
|
146
130
|
* - bearerAuth: []
|
|
147
|
-
* {{else}}
|
|
148
|
-
* security:
|
|
149
|
-
* - oidcAuth: []
|
|
150
|
-
* {{/if_eq}}
|
|
151
131
|
* parameters:
|
|
152
132
|
* - in: path
|
|
153
133
|
* name: id
|
|
@@ -205,13 +185,8 @@ const userUpdate = {
|
|
|
205
185
|
* delete:
|
|
206
186
|
* summary: 删除用户
|
|
207
187
|
* tags: [Users]
|
|
208
|
-
* {{#if_eq auth "static"}}
|
|
209
188
|
* security:
|
|
210
189
|
* - bearerAuth: []
|
|
211
|
-
* {{else}}
|
|
212
|
-
* security:
|
|
213
|
-
* - oidcAuth: []
|
|
214
|
-
* {{/if_eq}}
|
|
215
190
|
* parameters:
|
|
216
191
|
* - in: path
|
|
217
192
|
* name: id
|
|
@@ -21,44 +21,12 @@ export function buildOpenApiSpec() {
|
|
|
21
21
|
],
|
|
22
22
|
components: {
|
|
23
23
|
securitySchemes: {
|
|
24
|
-
{{#if_eq auth "static"}}
|
|
25
24
|
bearerAuth: {
|
|
26
25
|
type: 'http',
|
|
27
26
|
scheme: 'bearer',
|
|
28
|
-
bearerFormat: 'JWT'
|
|
27
|
+
bearerFormat: 'JWT',
|
|
28
|
+
description: 'Paste your JWT access token (use `npm run get-token` to fetch one).'
|
|
29
29
|
}
|
|
30
|
-
{{else}}
|
|
31
|
-
oidcAuth: (() => {
|
|
32
|
-
// Resolve OIDC endpoints from OIDC_ISSUER or OIDC_JWKS_URI
|
|
33
|
-
// Supports Keycloak realms and generic OIDC providers
|
|
34
|
-
const issuer = process.env.OIDC_ISSUER?.replace(/\/$/, '')
|
|
35
|
-
const jwksUri = process.env.OIDC_JWKS_URI
|
|
36
|
-
let baseUrl
|
|
37
|
-
if (issuer) {
|
|
38
|
-
baseUrl = issuer
|
|
39
|
-
} else if (jwksUri) {
|
|
40
|
-
// Keycloak JWKS URI: https://host/realms/<name>/protocol/openid-connect/certs
|
|
41
|
-
const match = jwksUri.match(/^(https?:\/\/[^/]+\/realms\/[^/]+)/)
|
|
42
|
-
if (match) baseUrl = match[1]
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
type: 'oauth2',
|
|
46
|
-
flows: {
|
|
47
|
-
authorizationCode: {
|
|
48
|
-
authorizationUrl: baseUrl ? `${baseUrl}/protocol/openid-connect/auth` : undefined,
|
|
49
|
-
tokenUrl: baseUrl ? `${baseUrl}/protocol/openid-connect/token` : undefined,
|
|
50
|
-
refreshUrl: baseUrl ? `${baseUrl}/protocol/openid-connect/token` : undefined,
|
|
51
|
-
scopes: {
|
|
52
|
-
openid: 'OpenID Connect scope',
|
|
53
|
-
profile: 'User profile',
|
|
54
|
-
email: 'Email address',
|
|
55
|
-
offline_access: 'Offline token refresh'
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
})()
|
|
61
|
-
{{/if_eq}}
|
|
62
30
|
}
|
|
63
31
|
}
|
|
64
32
|
},
|
|
@@ -44,7 +44,7 @@ SWAGGER_ENABLED=true
|
|
|
44
44
|
JWT_SECRET=please-change-this-in-production
|
|
45
45
|
JWT_EXPIRES_IN=7d
|
|
46
46
|
{{else}}
|
|
47
|
-
# ---------- Auth: jwks (OIDC compatible) ----------
|
|
47
|
+
# ---------- Auth: jwks (OIDC compatible, public-key signature) ----------
|
|
48
48
|
# Works with any OIDC-compliant IdP. Common issuer formats:
|
|
49
49
|
# Keycloak: https://kc.example.com/realms/myrealm
|
|
50
50
|
# Auth0: https://your-tenant.auth0.com/
|
|
@@ -53,9 +53,15 @@ JWT_EXPIRES_IN=7d
|
|
|
53
53
|
# `audience` is the client id of this API in the IdP (recommended).
|
|
54
54
|
# JWKS URI is auto-derived as ${OIDC_ISSUER}/.well-known/jwks.json
|
|
55
55
|
# unless OIDC_JWKS_URI is set explicitly.
|
|
56
|
+
# This API only verifies the JWT signature; it does not handle the OAuth flow.
|
|
57
|
+
# Use `npm run get-token` to obtain a token for local testing.
|
|
56
58
|
OIDC_ISSUER=
|
|
57
59
|
OIDC_AUDIENCE=
|
|
58
60
|
OIDC_JWKS_URI=
|
|
61
|
+
# The following are read by scripts/get-token.ts to fetch a test token:
|
|
62
|
+
OIDC_CLIENT_ID=
|
|
63
|
+
OIDC_USERNAME=
|
|
64
|
+
OIDC_PASSWORD=
|
|
59
65
|
{{/if_eq}}
|
|
60
66
|
|
|
61
67
|
# ---------- Logging ----------
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"cron:dev": "npx dotenvx run -- tsx watch src/cron-app.ts",
|
|
14
14
|
"seed": "npx dotenvx run -- tsx src/commands/seed.ts",
|
|
15
15
|
"migrate": "npx dotenvx run -- tsx src/commands/migrate.ts",
|
|
16
|
+
"get-token": "tsx scripts/get-token.ts",
|
|
16
17
|
"build": "tsc && rollup -c rollup.config.mjs",
|
|
17
18
|
"build:dev": "rollup -c rollup.config.mjs --environment NODE_ENV:development",
|
|
18
19
|
"lint": "eslint --fix --ext .ts src",
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"@dotenvx/dotenvx": "^1.75.1",
|
|
27
|
+
"axios": "^1.7.7",
|
|
26
28
|
"http-errors": "^2.0.1",
|
|
27
29
|
"skweb-express": "^2.1.0",
|
|
28
30
|
"skweb-express-jwt": "^3.0.0",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get-token.ts
|
|
3
|
+
* -----------------------------------------------------------------------------
|
|
4
|
+
* Fetch a Keycloak access token using Resource Owner Password Credentials grant.
|
|
5
|
+
* Reads configuration from the project .env (via dotenv) and prints the token
|
|
6
|
+
* to stdout so you can pipe it into curl / paste it into Swagger UI.
|
|
7
|
+
*
|
|
8
|
+
* Required env vars in .env:
|
|
9
|
+
* OIDC_ISSUER - e.g. https://kc.example.com/realms/myrealm
|
|
10
|
+
* OIDC_CLIENT_ID - the public client id (must enable "Direct access grants")
|
|
11
|
+
* OIDC_USERNAME - the test user
|
|
12
|
+
* OIDC_PASSWORD - the test user's password
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* tsx scripts/get-token.ts
|
|
16
|
+
* TOKEN=$(tsx scripts/get-token.ts)
|
|
17
|
+
* curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/users
|
|
18
|
+
*/
|
|
19
|
+
import '@dotenvx/dotenvx/config'
|
|
20
|
+
import axios from 'axios'
|
|
21
|
+
|
|
22
|
+
async function getToken(): Promise<string> {
|
|
23
|
+
const issuer = (process.env.OIDC_ISSUER || '').replace(/\/$/, '')
|
|
24
|
+
const clientId = process.env.OIDC_CLIENT_ID
|
|
25
|
+
const username = process.env.OIDC_USERNAME
|
|
26
|
+
const password = process.env.OIDC_PASSWORD
|
|
27
|
+
|
|
28
|
+
if (!issuer) throw new Error('OIDC_ISSUER is missing in .env')
|
|
29
|
+
if (!clientId) throw new Error('OIDC_CLIENT_ID is missing in .env')
|
|
30
|
+
if (!username) throw new Error('OIDC_USERNAME is missing in .env')
|
|
31
|
+
if (!password) throw new Error('OIDC_PASSWORD is missing in .env')
|
|
32
|
+
|
|
33
|
+
const params = new URLSearchParams({
|
|
34
|
+
grant_type: 'password',
|
|
35
|
+
client_id: clientId,
|
|
36
|
+
username,
|
|
37
|
+
password
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const tokenUrl = `${issuer}/protocol/openid-connect/token`
|
|
41
|
+
const response = await axios.post(tokenUrl, params.toString(), {
|
|
42
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return response.data.access_token
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getToken()
|
|
49
|
+
.then((token) => {
|
|
50
|
+
process.stdout.write(token + '\n')
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
console.error('❌ Failed to fetch token:', err.response?.data || err.message)
|
|
54
|
+
process.exit(1)
|
|
55
|
+
})
|
|
@@ -7,13 +7,8 @@ import { SysUser } from '../models/SysUser.js'
|
|
|
7
7
|
* get:
|
|
8
8
|
* summary: 获取用户列表
|
|
9
9
|
* tags: [Users]
|
|
10
|
-
* {{#if_eq auth "static"}}
|
|
11
10
|
* security:
|
|
12
11
|
* - bearerAuth: []
|
|
13
|
-
* {{else}}
|
|
14
|
-
* security:
|
|
15
|
-
* - oidcAuth: []
|
|
16
|
-
* {{/if_eq}}
|
|
17
12
|
* parameters:
|
|
18
13
|
* - in: query
|
|
19
14
|
* name: page
|
|
@@ -50,13 +45,8 @@ const userList = {
|
|
|
50
45
|
* get:
|
|
51
46
|
* summary: 获取单个用户
|
|
52
47
|
* tags: [Users]
|
|
53
|
-
* {{#if_eq auth "static"}}
|
|
54
48
|
* security:
|
|
55
49
|
* - bearerAuth: []
|
|
56
|
-
* {{else}}
|
|
57
|
-
* security:
|
|
58
|
-
* - oidcAuth: []
|
|
59
|
-
* {{/if_eq}}
|
|
60
50
|
* parameters:
|
|
61
51
|
* - in: path
|
|
62
52
|
* name: id
|
|
@@ -93,13 +83,8 @@ const userGet = {
|
|
|
93
83
|
* post:
|
|
94
84
|
* summary: 创建用户
|
|
95
85
|
* tags: [Users]
|
|
96
|
-
* {{#if_eq auth "static"}}
|
|
97
86
|
* security:
|
|
98
87
|
* - bearerAuth: []
|
|
99
|
-
* {{else}}
|
|
100
|
-
* security:
|
|
101
|
-
* - oidcAuth: []
|
|
102
|
-
* {{/if_eq}}
|
|
103
88
|
* requestBody:
|
|
104
89
|
* required: true
|
|
105
90
|
* content:
|
|
@@ -141,13 +126,8 @@ const userCreate = {
|
|
|
141
126
|
* put:
|
|
142
127
|
* summary: 更新用户
|
|
143
128
|
* tags: [Users]
|
|
144
|
-
* {{#if_eq auth "static"}}
|
|
145
129
|
* security:
|
|
146
130
|
* - bearerAuth: []
|
|
147
|
-
* {{else}}
|
|
148
|
-
* security:
|
|
149
|
-
* - oidcAuth: []
|
|
150
|
-
* {{/if_eq}}
|
|
151
131
|
* parameters:
|
|
152
132
|
* - in: path
|
|
153
133
|
* name: id
|
|
@@ -205,13 +185,8 @@ const userUpdate = {
|
|
|
205
185
|
* delete:
|
|
206
186
|
* summary: 删除用户
|
|
207
187
|
* tags: [Users]
|
|
208
|
-
* {{#if_eq auth "static"}}
|
|
209
188
|
* security:
|
|
210
189
|
* - bearerAuth: []
|
|
211
|
-
* {{else}}
|
|
212
|
-
* security:
|
|
213
|
-
* - oidcAuth: []
|
|
214
|
-
* {{/if_eq}}
|
|
215
190
|
* parameters:
|
|
216
191
|
* - in: path
|
|
217
192
|
* name: id
|
|
@@ -21,44 +21,12 @@ export function buildOpenApiSpec(): object {
|
|
|
21
21
|
],
|
|
22
22
|
components: {
|
|
23
23
|
securitySchemes: {
|
|
24
|
-
{{#if_eq auth "static"}}
|
|
25
24
|
bearerAuth: {
|
|
26
25
|
type: 'http',
|
|
27
26
|
scheme: 'bearer',
|
|
28
|
-
bearerFormat: 'JWT'
|
|
27
|
+
bearerFormat: 'JWT',
|
|
28
|
+
description: 'Paste your JWT access token (use `npm run get-token` to fetch one).'
|
|
29
29
|
}
|
|
30
|
-
{{else}}
|
|
31
|
-
oidcAuth: (() => {
|
|
32
|
-
// Resolve OIDC endpoints from OIDC_ISSUER or OIDC_JWKS_URI
|
|
33
|
-
// Supports Keycloak realms and generic OIDC providers
|
|
34
|
-
const issuer = process.env.OIDC_ISSUER?.replace(/\/$/, '')
|
|
35
|
-
const jwksUri = process.env.OIDC_JWKS_URI
|
|
36
|
-
let baseUrl: string | undefined
|
|
37
|
-
if (issuer) {
|
|
38
|
-
baseUrl = issuer
|
|
39
|
-
} else if (jwksUri) {
|
|
40
|
-
// Keycloak JWKS URI: https://host/realms/<name>/protocol/openid-connect/certs
|
|
41
|
-
const match = jwksUri.match(/^(https?:\/\/[^/]+\/realms\/[^/]+)/)
|
|
42
|
-
if (match) baseUrl = match[1]
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
type: 'oauth2',
|
|
46
|
-
flows: {
|
|
47
|
-
authorizationCode: {
|
|
48
|
-
authorizationUrl: baseUrl ? `${baseUrl}/protocol/openid-connect/auth` : undefined,
|
|
49
|
-
tokenUrl: baseUrl ? `${baseUrl}/protocol/openid-connect/token` : undefined,
|
|
50
|
-
refreshUrl: baseUrl ? `${baseUrl}/protocol/openid-connect/token` : undefined,
|
|
51
|
-
scopes: {
|
|
52
|
-
openid: 'OpenID Connect scope',
|
|
53
|
-
profile: 'User profile',
|
|
54
|
-
email: 'Email address',
|
|
55
|
-
offline_access: 'Offline token refresh'
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
})()
|
|
61
|
-
{{/if_eq}}
|
|
62
30
|
}
|
|
63
31
|
}
|
|
64
32
|
},
|