create-skweb 3.4.0 โ 3.7.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 +4 -4
- package/.turbo/turbo-test.log +51 -34
- package/CHANGELOG.md +30 -0
- package/package.json +1 -1
- package/templates/cjs/.env.example +7 -1
- package/templates/cjs/package.json +4 -2
- 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 +4 -2
- 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 +4 -2
- 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,16 @@
|
|
|
1
1
|
|
|
2
|
-
> create-skweb@3.
|
|
2
|
+
> create-skweb@3.7.0 prebuild
|
|
3
3
|
> tsc --outDir dist --rootDir src
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> create-skweb@3.
|
|
6
|
+
> create-skweb@3.7.0 build
|
|
7
7
|
> rollup -c rollup.config.mjs && node scripts/inject-shebang.mjs && chmod +x dist/cli.js
|
|
8
8
|
|
|
9
9
|
[36m
|
|
10
10
|
[1msrc/index.ts[22m โ [1mdist/index.js[22m...[39m
|
|
11
11
|
[1m[33m(!) [plugin typescript] @rollup/plugin-typescript: outputToFilesystem option is defaulting to true.[39m[22m
|
|
12
|
-
[32mcreated [1mdist/index.js[22m in [
|
|
12
|
+
[32mcreated [1mdist/index.js[22m in [1m387ms[22m[39m
|
|
13
13
|
[36m
|
|
14
14
|
[1msrc/cli.ts[22m โ [1mdist/cli.js[22m...[39m
|
|
15
|
-
[32mcreated [1mdist/cli.js[22m in [
|
|
15
|
+
[32mcreated [1mdist/cli.js[22m in [1m193ms[22m[39m
|
|
16
16
|
[inject-shebang] Shebang added to /Users/stayknight/projects/skweb-mono/tools/create-skweb/dist/cli.js
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
|
|
2
|
-
> create-skweb@3.
|
|
2
|
+
> create-skweb@3.5.0 test
|
|
3
3
|
> mocha test/**/*.mjs
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
create-skweb
|
|
8
8
|
createProject()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
1) should create TypeScript project from template
|
|
10
|
+
2) should create MJS project from template
|
|
11
|
+
3) should create CJS project from template
|
|
12
12
|
โ should reject non-empty destination directory
|
|
13
13
|
โ should include winston in all templates
|
|
14
14
|
โ should include @dotenvx/dotenvx in all templates
|
|
@@ -23,47 +23,64 @@
|
|
|
23
23
|
โ should not throw
|
|
24
24
|
printSuccess()
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
โ ้กน็ฎๅๅปบๆๅ๏ผ โ
|
|
28
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
29
|
-
|
|
30
|
-
้กน็ฎๅ็งฐ: my-app
|
|
31
|
-
้กน็ฎ่ทฏๅพ: /tmp/my-app/my-app
|
|
26
|
+
้กน็ฎๅๅปบๆๅ๏ผ
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
ๅ็งฐ: my-app
|
|
29
|
+
่ทฏๅพ: /tmp/my-app/my-app
|
|
34
30
|
|
|
31
|
+
ๆฅไธๆฅๆง่ก:
|
|
35
32
|
cd my-app
|
|
36
|
-
|
|
37
|
-
# ๅคๅถ็ฏๅขๅ้ๆจกๆฟๅนถๆ้ไฟฎๆน
|
|
38
33
|
cp .env.example .env
|
|
34
|
+
npm run web:dev
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
npm run start:dev
|
|
36
|
+
โ should not throw
|
|
42
37
|
|
|
43
|
-
# ็ไบงๆจกๅผ่ฟ่ก (้่ฆๅ
ๆๅปบ)
|
|
44
|
-
npm run build
|
|
45
|
-
npm run start
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
7 passing (131ms)
|
|
40
|
+
3 failing
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
1) create-skweb
|
|
43
|
+
createProject()
|
|
44
|
+
should create TypeScript project from template:
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
AssertionError: expected false to be true
|
|
47
|
+
+ expected - actual
|
|
55
48
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
โ
SKWeb Express ้ๆ
|
|
61
|
-
โ
Keycloak/JWKS ้ดๆ (ๅฏ้)
|
|
62
|
-
โ
ESLint ไปฃ็ ๆฃๆฅ
|
|
63
|
-
โ
Mocha ๆต่ฏๆกๆถ
|
|
49
|
+
-false
|
|
50
|
+
+true
|
|
51
|
+
|
|
52
|
+
at Context.<anonymous> (file:///Users/stayknight/projects/skweb-mono/tools/create-skweb/test/index.mjs:38:68)
|
|
64
53
|
|
|
65
|
-
|
|
54
|
+
2) create-skweb
|
|
55
|
+
createProject()
|
|
56
|
+
should create MJS project from template:
|
|
57
|
+
|
|
58
|
+
AssertionError: expected false to be true
|
|
59
|
+
+ expected - actual
|
|
60
|
+
|
|
61
|
+
-false
|
|
62
|
+
+true
|
|
63
|
+
|
|
64
|
+
at Context.<anonymous> (file:///Users/stayknight/projects/skweb-mono/tools/create-skweb/test/index.mjs:51:68)
|
|
65
|
+
|
|
66
|
+
3) create-skweb
|
|
67
|
+
createProject()
|
|
68
|
+
should create CJS project from template:
|
|
69
|
+
|
|
70
|
+
AssertionError: expected false to be true
|
|
71
|
+
+ expected - actual
|
|
72
|
+
|
|
73
|
+
-false
|
|
74
|
+
+true
|
|
75
|
+
|
|
76
|
+
at Context.<anonymous> (file:///Users/stayknight/projects/skweb-mono/tools/create-skweb/test/index.mjs:63:68)
|
|
66
77
|
|
|
67
78
|
|
|
68
|
-
10 passing (179ms)
|
|
69
79
|
|
|
80
|
+
npm error Lifecycle script `test` failed with error:
|
|
81
|
+
npm error code 3
|
|
82
|
+
npm error path /Users/stayknight/projects/skweb-mono/tools/create-skweb
|
|
83
|
+
npm error workspace create-skweb@3.5.0
|
|
84
|
+
npm error location /Users/stayknight/projects/skweb-mono/tools/create-skweb
|
|
85
|
+
npm error command failed
|
|
86
|
+
npm error command sh -c mocha test/**/*.mjs
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# create-skweb
|
|
2
2
|
|
|
3
|
+
## 3.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 39b301f: feat(skweb-express-jwt): ้ๆ jwks ๆจกๅผ๏ผๅ ้ค verifyAsync/getKey/createJwksClient
|
|
8
|
+
|
|
9
|
+
- ๅ ้ค `verifyAsync()`๏ผjwks ๆจกๅผไธ็จไธๅฐ๏ผ่ฏท็จ `midware` ๅ่ฏทๆฑ็บง้ช่ฏ
|
|
10
|
+
- ๅ ้ค `getKey()` / `createJwksClient()` / `this.jwksClient`๏ผไธๅ็ผๅญ็ฌ็ซ jwks ๅฎขๆท็ซฏ
|
|
11
|
+
- `createMidware()` ๆน็จ `jwksRsa.expressJwtSecret` ๆ ๅๅฎ็ฐ๏ผไนๅๆฏๆๅ kid ๆๅ้ป่พ๏ผ
|
|
12
|
+
- `verify()` ้่ฏฏๆถๆฏๆดๆฐ๏ผๆ็คบ็จๆทๆน็จ `midware` ่ไธๆฏ `verifyAsync`
|
|
13
|
+
- ่ฟๆฏ breaking change๏ผ่ฐ็จๆนๅฆๆ็จไบ `verifyAsync()`๏ผ่ฏท่ฟ็งปๅฐ `midware`
|
|
14
|
+
|
|
15
|
+
ไพ่ตๅ็บง๏ผ
|
|
16
|
+
|
|
17
|
+
- `skweb-express@2.2.0` ๅผ็จ `skweb-express-jwt@^4.0.0`
|
|
18
|
+
- `create-skweb@3.6.0` ๆจกๆฟๅๆญฅๅฐ `skweb-express@^2.2.0` ๅ `skweb-express-jwt@^4.0.0`
|
|
19
|
+
|
|
20
|
+
## 3.5.0
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- a43c261: feat: ็ฎๅๆจกๆฟ auth/swagger ้
็ฝฎ๏ผๆทปๅ get-token ่ๆฌ
|
|
25
|
+
|
|
26
|
+
- Swagger ็ปไธไฝฟ็จ bearerAuth (http + bearer + JWT)๏ผไธๅไฝฟ็จ oauth2 ๆต็จ
|
|
27
|
+
- ็งป้ค clientSecret ็ธๅ
ณ้
็ฝฎ
|
|
28
|
+
- ๆทปๅ scripts/get-token.{js,ts}๏ผไป Keycloak (ROPC grant) ่ทๅ access token
|
|
29
|
+
- package.json ๆทปๅ get-token ่ๆฌๅ axios ไพ่ต
|
|
30
|
+
- .env.example ๆทปๅ OIDC_CLIENT_ID/OIDC_USERNAME/OIDC_PASSWORD (ไพ get-token ไฝฟ็จ)
|
|
31
|
+
- ๆๆไธไธชๆจกๆฟ๏ผts/mjs/cjs๏ผๅทฒๅๆญฅ
|
|
32
|
+
|
|
3
33
|
## 3.3.9
|
|
4
34
|
|
|
5
35
|
### 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,9 +22,10 @@
|
|
|
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
|
-
"skweb-express": "^2.
|
|
26
|
-
"skweb-express-jwt": "^
|
|
27
|
+
"skweb-express": "^2.2.0",
|
|
28
|
+
"skweb-express-jwt": "^4.0.0",
|
|
27
29
|
"skweb-express-logger": "^2.0.0",
|
|
28
30
|
"skweb-sequelize": "^2.0.1",
|
|
29
31
|
"skweb-redis": "^2.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,9 +22,10 @@
|
|
|
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
|
-
"skweb-express": "^2.
|
|
26
|
-
"skweb-express-jwt": "^
|
|
27
|
+
"skweb-express": "^2.2.0",
|
|
28
|
+
"skweb-express-jwt": "^4.0.0",
|
|
27
29
|
"skweb-express-logger": "^2.0.0",
|
|
28
30
|
"skweb-sequelize": "^2.0.1",
|
|
29
31
|
"skweb-redis": "^2.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,9 +24,10 @@
|
|
|
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
|
-
"skweb-express": "^2.
|
|
28
|
-
"skweb-express-jwt": "^
|
|
29
|
+
"skweb-express": "^2.2.0",
|
|
30
|
+
"skweb-express-jwt": "^4.0.0",
|
|
29
31
|
"skweb-express-logger": "^2.0.0",
|
|
30
32
|
"skweb-sequelize": "^2.0.1",
|
|
31
33
|
"skweb-redis": "^2.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
|
},
|