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.
@@ -1,16 +1,16 @@
1
1
 
2
- > create-skweb@3.3.9 prebuild
2
+ > create-skweb@3.7.0 prebuild
3
3
  > tsc --outDir dist --rootDir src
4
4
 
5
5
 
6
- > create-skweb@3.3.9 build
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
  
10
10
  src/index.ts โ†’ dist/index.js...
11
11
  (!) [plugin typescript] @rollup/plugin-typescript: outputToFilesystem option is defaulting to true.
12
- created dist/index.js in 391ms
12
+ created dist/index.js in 387ms
13
13
  
14
14
  src/cli.ts โ†’ dist/cli.js...
15
- created dist/cli.js in 214ms
15
+ created dist/cli.js in 193ms
16
16
  [inject-shebang] Shebang added to /Users/stayknight/projects/skweb-mono/tools/create-skweb/dist/cli.js
@@ -1,14 +1,14 @@
1
1
 
2
- > create-skweb@3.2.0 test
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
- โœ” should create TypeScript project from template
10
- โœ” should create MJS project from template
11
- โœ” should create CJS project from template
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
- npm run logs
39
+ 7 passing (131ms)
40
+ 3 failing
49
41
 
50
- # ๅœๆญขๆœๅŠก
51
- npm run stop
42
+ 1) create-skweb
43
+ createProject()
44
+ should create TypeScript project from template:
52
45
 
53
- # ้‡ๅฏๆœๅŠก
54
- npm run restart
46
+ AssertionError: expected false to be true
47
+ + expected - actual
55
48
 
56
- ้กน็›ฎ็‰นๆ€ง:
57
- โœ… TypeScript/MJS/CJS ๆ”ฏๆŒ
58
- โœ… @dotenvx/dotenvx ็Žฏๅขƒๅ˜้‡็ฎก็†
59
- โœ… PM2 ่ฟ›็จ‹็ฎก็†
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
- โœ” should not throw
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-skweb",
3
- "version": "3.4.0",
3
+ "version": "3.7.0",
4
4
  "description": "Create a new SKWeb project with TypeScript, CJS, or MJS support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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.1.0",
26
- "skweb-express-jwt": "^3.0.0",
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: https://your-domain.okta.com/oauth2/default
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.1.0",
26
- "skweb-express-jwt": "^3.0.0",
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.1.0",
28
- "skweb-express-jwt": "^3.0.0",
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
  },