Compare commits
456 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d24a8e99cc | ||
|
59d2d56091 | ||
|
8515b7060a | ||
|
a076b3f4d0 | ||
|
1e3240ef35 | ||
|
d767080dfe | ||
|
b228eda488 | ||
|
35f54964ce | ||
|
759b834ccc | ||
|
137e12cbbb | ||
|
57b08d9dea | ||
|
1dd7b673a1 | ||
|
b00ffc50c3 | ||
|
f9db40d33d | ||
|
4bc6a843d8 | ||
|
da3e7514f0 | ||
|
bc396bc41b | ||
|
947efe8d63 | ||
|
64189bf8fe | ||
|
400c630418 | ||
|
402360b436 | ||
|
9f001f1521 | ||
|
368e3a32c5 | ||
|
002e3d4b3d | ||
|
f94f9b51c9 | ||
|
055b15bd46 | ||
|
0e70b0b0de | ||
|
8faf9a3eed | ||
|
075d9f9de5 | ||
|
840079be32 | ||
|
50ae962f09 | ||
|
61aa1f9896 | ||
|
d88e0e4dd5 | ||
|
18c36eb4de | ||
|
80aeabad51 | ||
|
419e1f473a | ||
|
80988125e8 | ||
|
271ad2da71 | ||
|
b2732f2595 | ||
|
53820e1e34 | ||
|
09dd45e437 | ||
|
1f654a7820 | ||
|
0690f40bad | ||
|
2285883149 | ||
|
af87e41bb8 | ||
|
9ba884483d | ||
|
f5a300953a | ||
|
ab9a962f58 | ||
|
484adb607f | ||
|
e1f38d4196 | ||
|
5de629acf2 | ||
|
8b4b24a036 | ||
|
75ab130249 | ||
|
981ca7e9a4 | ||
|
acb4e260a7 | ||
|
ff20b0a844 | ||
|
1b77c69a01 | ||
|
158275f5c2 | ||
|
a085c8093e | ||
|
cb358bd745 | ||
|
e788c46601 | ||
|
d551b4bffb | ||
|
c168c7b156 | ||
|
7a46115042 | ||
|
249a7bde89 | ||
|
813740a002 | ||
|
7840c2a6f5 | ||
|
8f6c0d36d9 | ||
|
12690b892b | ||
|
d01b4b71c9 | ||
|
c29e600786 | ||
|
6309b7c45d | ||
|
7e7996e40c | ||
|
deaeab0f61 | ||
|
6bd5451230 | ||
|
fb2d651a6f | ||
|
4845d7c32d | ||
|
c33c315120 | ||
|
99b8f1e789 | ||
|
6af13e1405 | ||
|
f59fa4238c | ||
|
248effc57d | ||
|
9e540b2c1f | ||
|
ab7b5ff490 | ||
|
486f944e0f | ||
|
6cc3d4c442 | ||
|
083290c6d4 | ||
|
cd1b55b850 | ||
|
482ba6c639 | ||
|
e2921b7e37 | ||
|
c87b6153bb | ||
|
488dd2c6b9 | ||
|
1ac678a368 | ||
|
5866c802e5 | ||
|
fe892c840b | ||
|
9685dfb55a | ||
|
c1dc899bc1 | ||
|
d2da43c617 | ||
|
6de5fd4f96 | ||
|
cc3d0d61dd | ||
|
4403f00274 | ||
|
eddfb8e634 | ||
|
4f2790f6d3 | ||
|
96690e1354 | ||
|
982f216a01 | ||
|
13c21e8910 | ||
|
94b7d2b85b | ||
|
9a4f89e69d | ||
|
a5ba03cca0 | ||
|
5203813e7b | ||
|
0e461fd072 | ||
|
326411ca5d | ||
|
c39c450e90 | ||
|
3191954dda | ||
|
20c6d2ea86 | ||
|
f43544e134 | ||
|
474a863708 | ||
|
0bacdca8fe | ||
|
f023d6bca7 | ||
|
150b01f1f3 | ||
|
2b2bb20658 | ||
|
70570faed6 | ||
|
5d3b7c9a82 | ||
|
95b9b12a3b | ||
|
0e027055cb | ||
|
e47b002535 | ||
|
8dd6dcd1fc | ||
|
10cfa1cf41 | ||
|
3938584aeb | ||
|
163b060dc5 | ||
|
67f8ae41fc | ||
|
b6e9fe2585 | ||
|
5b83bd8fa9 | ||
|
d0f43b6318 | ||
|
df338ed6a0 | ||
|
295994d02a | ||
|
f9e15f93c4 | ||
|
2659cf391c | ||
|
76416ddd5b | ||
|
8e8a0ccf54 | ||
|
db0ec954df | ||
|
dc942aa5de | ||
|
029cfcb591 | ||
|
b1b6919395 | ||
|
9185740d35 | ||
|
8d59fe7b67 | ||
|
92b505dd56 | ||
|
c0e6661d3d | ||
|
0cae2c68d8 | ||
|
1e43343529 | ||
|
0a74dca7c2 | ||
|
a66a373256 | ||
|
606cf7ad02 | ||
|
5225749c7b | ||
|
819e3833ad | ||
|
b0ba37fcc4 | ||
|
f4829b557f | ||
|
60a9b5a693 | ||
|
c323b94a8c | ||
|
4bbc0241ec | ||
|
5a7dacfcdd | ||
|
7e05e1bf0c | ||
|
1530ca32c8 | ||
|
ed054f131a | ||
|
ec74ceef4d | ||
|
fd3261cdf1 | ||
|
b4b53d2552 | ||
|
0371d9ea7a | ||
|
73031e74ec | ||
|
f71ab4635f | ||
|
983db6780a | ||
|
ea22107b9b | ||
|
8e4a7fed9e | ||
|
30efd8fcf4 | ||
|
f4c4e92ca1 | ||
|
cfda99f5b0 | ||
|
5063b1c7ab | ||
|
955b6cac45 | ||
|
1fa3d6133c | ||
|
023939a064 | ||
|
de4490cf0f | ||
|
e0cda43724 | ||
|
d4ac67dba8 | ||
|
23f4b59559 | ||
|
4a7ba4a1c9 | ||
|
a3e765e1fc | ||
|
ee3614dbf1 | ||
|
8099ec1ffe | ||
|
2e2b0ab3ae | ||
|
a1f15f2f6b | ||
|
8fe74fe7ee | ||
|
232be50225 | ||
|
a00bb0c4db | ||
|
5517d6baf4 | ||
|
e2975503a4 | ||
|
1231c92198 | ||
|
64ca0bc0ca | ||
|
2bceb9385c | ||
|
954bc43c22 | ||
|
8c99aa4b9d | ||
|
884195d30d | ||
|
17dd726158 | ||
|
7ee30b6a42 | ||
|
397f8133bf | ||
|
c9abc6dc5c | ||
|
e78eafd1f1 | ||
|
e50f2bd692 | ||
|
ed0428bd55 | ||
|
2a126d65c5 | ||
|
30149ff1f2 | ||
|
32f471624a | ||
|
7f5eabb639 | ||
|
32fb1e2f71 | ||
|
7c3c6d3643 | ||
|
b6d2154d56 | ||
|
b8d4daf4c1 | ||
|
c03623875a | ||
|
c423e4cacc | ||
|
c593f5cb97 | ||
|
2ad36a8137 | ||
|
bccaddc2de | ||
|
97c12c8a12 | ||
|
b349a819ba | ||
|
792d4262c8 | ||
|
37a9046a40 | ||
|
5ad29d9f43 | ||
|
645031543e | ||
|
b43c02f279 | ||
|
63877d53be | ||
|
79a580b4a5 | ||
|
994f1f8d3d | ||
|
a73281d46d | ||
|
a60a0d0696 | ||
|
a2ea81b3b8 | ||
|
a0461e3ef0 | ||
|
93fcf96cde | ||
|
d7455fd100 | ||
|
af7a45d125 | ||
|
5357626317 | ||
|
552c95c59e | ||
|
50590697ca | ||
|
e261736fa3 | ||
|
db37320280 | ||
|
263f55fdd3 | ||
|
2b7f8a24a3 | ||
|
b0aa0feab5 | ||
|
0e93d4ed09 | ||
|
dc4eac1a04 | ||
|
53a427d190 | ||
|
ae969f91ac | ||
|
c83319d6f3 | ||
|
329b4cb0fb | ||
|
533f5992d1 | ||
|
cb103cc3e2 | ||
|
afdf89fb12 | ||
|
26a87b8eaa | ||
|
25d31463f4 | ||
|
2542c38c9b | ||
|
7326038424 | ||
|
12e632d221 | ||
|
3fc2108214 | ||
|
babbe39494 | ||
|
32b4585e39 | ||
|
43469a869c | ||
|
6f823e6478 | ||
|
e33693e20e | ||
|
ad3f091d4a | ||
|
3ff01d186d | ||
|
0cf8caa53b | ||
|
25920c208d | ||
|
19da96113f | ||
|
6e584e809e | ||
|
4185188a5b | ||
|
4273322ed5 | ||
|
ba0532c95d | ||
|
3a2fe597ba | ||
|
dda77952a0 | ||
|
d2e518d96b | ||
|
f3a97b2538 | ||
|
cacd017244 | ||
|
f5b15a5ef6 | ||
|
de620dca56 | ||
|
8decf4a3c9 | ||
|
831326952b | ||
|
27da578446 | ||
|
2c1cca168f | ||
|
e498fb784b | ||
|
2917719315 | ||
|
9ed90995e4 | ||
|
0f99bb5bdc | ||
|
1f4d4473e4 | ||
|
5332001ff4 | ||
|
22241fd7ad | ||
|
ddeade9775 | ||
|
f1cb165bdd | ||
|
9873353990 | ||
|
b879569b81 | ||
|
c3e821088b | ||
|
dc4f386e7a | ||
|
a40810b364 | ||
|
1690629717 | ||
|
9a6a71c8bc | ||
|
d626699f08 | ||
|
6aa60e685b | ||
|
9910762abc | ||
|
182fb430f1 | ||
|
bda20e2fbe | ||
|
bc586eceef | ||
|
128b27221a | ||
|
2dfb4eda9d | ||
|
fed6af4dfe | ||
|
c4ee6cd85c | ||
|
8fc307bd8d | ||
|
9e9ffcd586 | ||
|
49b064b5f2 | ||
|
ef6f5a4c23 | ||
|
e92562fd44 | ||
|
fe65ddb5f8 | ||
|
ffd405c6db | ||
|
9e41868e0d | ||
|
ca6accc889 | ||
|
dfe2e1562a | ||
|
ab43bb73d7 | ||
|
22b0b95e54 | ||
|
290ad8b592 | ||
|
d5519dbb55 | ||
|
4de9e059e9 | ||
|
e573aff6ae | ||
|
908e083dee | ||
|
ec29724997 | ||
|
88756e9807 | ||
|
80d8a0c4e3 | ||
|
7241f0c741 | ||
|
8565eb3fb8 | ||
|
87198f6e56 | ||
|
fa934da442 | ||
|
6c4dc711d2 | ||
|
1f2a755a97 | ||
|
a0e3dc163a | ||
|
810dc6ceb5 | ||
|
7203c742be | ||
|
2fd04cfb61 | ||
|
9c44d13f73 | ||
|
d6436a58a2 | ||
|
613167f3a6 | ||
|
ab0c281d98 | ||
|
c84dc281a2 | ||
|
835833a61b | ||
|
9af542ec89 | ||
|
06e33196bb | ||
|
36dd324139 | ||
|
52d4fe920c | ||
|
0d090d50d4 | ||
|
e57232edde | ||
|
dfe1e7ddd1 | ||
|
5ffc42c9db | ||
|
c63648a1b0 | ||
|
80fd4095c4 | ||
|
31d7a938f2 | ||
|
ee1b6868f8 | ||
|
7c7e5544e4 | ||
|
06fe26fbe7 | ||
|
c932c7d8f6 | ||
|
d56c536446 | ||
|
f5a9a28124 | ||
|
b86039536e | ||
|
59c4675e8a | ||
|
fbf6a329df | ||
|
ddec3118a4 | ||
|
94e2786297 | ||
|
ecfdff5454 | ||
|
c598a58ec9 | ||
|
3e38beb190 | ||
|
61a86e8e82 | ||
|
9c8e127fa0 | ||
|
eb23b40e5c | ||
|
9e19aafcd0 | ||
|
4cdf26b6f9 | ||
|
88de58cc22 | ||
|
f6b48c88b9 | ||
|
12534e57ad | ||
|
52e33bba2d | ||
|
d63e5f4e54 | ||
|
8d31866a0b | ||
|
7d818f217a | ||
|
7156665684 | ||
|
5045c5e8bf | ||
|
9de2ed9eaf | ||
|
096cf7a570 | ||
|
a04f07d149 | ||
|
63775b2866 | ||
|
e8609de7b4 | ||
|
e62aa89d72 | ||
|
77f12a940d | ||
|
0fe49e3d6e | ||
|
881386a123 | ||
|
7b9aec4ed0 | ||
|
cf18f9d06c | ||
|
052936f769 | ||
|
590de7a67b | ||
|
7f608ad8ad | ||
|
35b012b937 | ||
|
e3bd7f3bc5 | ||
|
e14f187fc0 | ||
|
da495b90ae | ||
|
8d9b80f549 | ||
|
2e9da33622 | ||
|
6f416ad335 | ||
|
4e982ee898 | ||
|
bcb514ae9c | ||
|
cfdad8d71d | ||
|
39ad1eb8d1 | ||
|
3d1d2e316b | ||
|
dd217e8a46 | ||
|
1339a96ea4 | ||
|
616e834c90 | ||
|
80eaa77acc | ||
|
ce3135c83b | ||
|
09abc73068 | ||
|
037d623550 | ||
|
8c8d2fd6a8 | ||
|
153a99d63f | ||
|
939e3ca7ea | ||
|
a0dc7eeb7c | ||
|
c21d072231 | ||
|
2e10ec8073 | ||
|
1177d6770c | ||
|
d492a393fe | ||
|
77952337c5 | ||
|
6716fcb881 | ||
|
302fe8d7cd | ||
|
2ea5e34e81 | ||
|
d7103b1ad9 | ||
|
9f8a36fe93 | ||
|
47ca811878 | ||
|
8cacb21f1b | ||
|
a0c95207cf | ||
|
da3023cf5e | ||
|
5892cf2ba7 | ||
|
c9d7fc1b6e | ||
|
94779c3476 | ||
|
802a6ab5ec | ||
|
04307c8226 | ||
|
ff6b91b801 | ||
|
c8ca598465 | ||
|
9444b897ee | ||
|
3d1951b72c | ||
|
acc27ae448 | ||
|
e6993214ff | ||
|
2f02a4379c | ||
|
b57d014e9a | ||
|
f57f311aab | ||
|
4f11415107 | ||
|
346fd7175f | ||
|
7c02d77057 | ||
|
d3fd4b6bbf |
@ -259,6 +259,110 @@
|
|||||||
"code",
|
"code",
|
||||||
"doc"
|
"doc"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "uchar",
|
||||||
|
"name": "Omid",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5172296?v=4",
|
||||||
|
"profile": "https://www.linkedin.com/in/oes-rioniz/",
|
||||||
|
"contributions": [
|
||||||
|
"test",
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "kathari00",
|
||||||
|
"name": "Katharina Eiserfey",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/42547712?v=4",
|
||||||
|
"profile": "https://github.com/kathari00",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"test",
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "luca-peruzzo",
|
||||||
|
"name": "Luca Peruzzo",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/69015314?v=4",
|
||||||
|
"profile": "https://github.com/luca-peruzzo",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "nima70",
|
||||||
|
"name": "Nima Shokouhfar",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5094767?v=4",
|
||||||
|
"profile": "https://github.com/nima70",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "marvinruder",
|
||||||
|
"name": "Marvin A. Ruder",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/18495294?v=4",
|
||||||
|
"profile": "https://mruder.dev",
|
||||||
|
"contributions": [
|
||||||
|
"bug"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "zvn2060",
|
||||||
|
"name": "HI_OuO",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/45450852?v=4",
|
||||||
|
"profile": "https://github.com/zvn2060",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "tripheo0412",
|
||||||
|
"name": "Tri Hoang",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/25382052?v=4",
|
||||||
|
"profile": "https://github.com/tripheo0412",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "EternalSide",
|
||||||
|
"name": "Lesha",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/118743608?v=4",
|
||||||
|
"profile": "http://t.me/AAT_L",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "bacongobbler",
|
||||||
|
"name": "Matthew Fisher",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1360539?v=4",
|
||||||
|
"profile": "https://blog.bacongobbler.com",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "kodebach",
|
||||||
|
"name": "Klemens Böswirth",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/23529132?v=4",
|
||||||
|
"profile": "https://github.com/kodebach",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "wnmzzzz",
|
||||||
|
"name": "wnmzzzz",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/117174301?v=4",
|
||||||
|
"profile": "https://github.com/wnmzzzz",
|
||||||
|
"contributions": [
|
||||||
|
"test"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
1
.github/FUNDING.yaml
vendored
1
.github/FUNDING.yaml
vendored
@ -1,4 +1,3 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: [garronej]
|
github: [garronej]
|
||||||
custom: ['https://www.ringerhq.com/experts/garronej']
|
|
||||||
|
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
storybook:
|
storybook:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
needs: test
|
needs: test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -112,7 +112,7 @@ jobs:
|
|||||||
registry-url: https://registry.npmjs.org/
|
registry-url: https://registry.npmjs.org/
|
||||||
- uses: bahmutov/npm-install@v1
|
- uses: bahmutov/npm-install@v1
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx -y -p denoify@1.6.12 enable_short_npm_import_path
|
- run: npx -y -p denoify@1.6.13 enable_short_npm_import_path
|
||||||
env:
|
env:
|
||||||
DRY_RUN: "0"
|
DRY_RUN: "0"
|
||||||
- uses: garronej/ts-ci@v2.1.2
|
- uses: garronej/ts-ci@v2.1.2
|
||||||
|
@ -12,4 +12,5 @@ node_modules/
|
|||||||
/sample_react_project/
|
/sample_react_project/
|
||||||
/sample_custom_react_project/
|
/sample_custom_react_project/
|
||||||
/keycloakify_starter_test/
|
/keycloakify_starter_test/
|
||||||
/.storybook/static/keycloak-resources/
|
/.storybook/static/keycloak-resources/
|
||||||
|
/src/bin/start-keycloak/*.json
|
@ -1,3 +1,3 @@
|
|||||||
Looking to contribute? Thank you! PR are more than welcome.
|
Looking to contribute? Thank you! PR are more than welcome.
|
||||||
|
|
||||||
Please refers to [this documentation page](https://docs.keycloakify.dev/contributing) that will help you get started.
|
Please refers to [this documentation page](https://docs.keycloakify.dev/faq-and-help/contributing) that will help you get started.
|
||||||
|
54
README.md
54
README.md
@ -6,7 +6,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://github.com/garronej/keycloakify/actions">
|
<a href="https://github.com/garronej/keycloakify/actions">
|
||||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
<img src="https://github.com/keycloakify/keycloakify/actions/workflows/ci.yaml/badge.svg">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.npmjs.com/package/keycloakify">
|
<a href="https://www.npmjs.com/package/keycloakify">
|
||||||
<img src="https://img.shields.io/npm/dm/keycloakify">
|
<img src="https://img.shields.io/npm/dm/keycloakify">
|
||||||
@ -41,23 +41,50 @@
|
|||||||
<img width="400" src="https://github.com/user-attachments/assets/6bf3bef9-00b0-4460-97b9-0d2da8500798">
|
<img width="400" src="https://github.com/user-attachments/assets/6bf3bef9-00b0-4460-97b9-0d2da8500798">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Keycloakify is fully compatible with Keycloak from version 11 to 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
||||||
|
|
||||||
|
> 📣 **Keycloakify 26 Released**
|
||||||
|
> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
|
||||||
|
> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
|
||||||
|
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/features/compiler-options/keycloakversiontargets).
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
Friends for the project, we trust and recommend their services.
|
Project backers, we trust and recommend their services.
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<i><a href="https://phasetwo.io/?utm_source=keycloakify"><strong>Keycloak as a Service</strong></a> - Keycloak community contributors of popular <a href="https://github.com/p2-inc#our-extensions-?utm_source=keycloakify">extensions</a> providing free and dedicated <a href="https://phasetwo.io/hosting/?utm_source=keycloakify">Keycloak hosting</a> and enterprise <a href="https://phasetwo.io/support/?utm_source=keycloakify">Keycloak support</a> to businesses of all sizes.</i>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -82,7 +109,7 @@ Friends for the project, we trust and recommend their services.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. </a>
|
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. Cloud IAM is a french company. </a>
|
||||||
<br/>
|
<br/>
|
||||||
Use code <code>keycloakify5</code> at checkout for a 5% discount.
|
Use code <code>keycloakify5</code> at checkout for a 5% discount.
|
||||||
</p>
|
</p>
|
||||||
@ -132,6 +159,21 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oliviergoulet5"><img src="https://avatars.githubusercontent.com/u/17685861?v=4?s=100" width="100px;" alt="Olivier Goulet"/><br /><sub><b>Olivier Goulet</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=oliviergoulet5" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oliviergoulet5"><img src="https://avatars.githubusercontent.com/u/17685861?v=4?s=100" width="100px;" alt="Olivier Goulet"/><br /><sub><b>Olivier Goulet</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=oliviergoulet5" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liamlows"><img src="https://avatars.githubusercontent.com/u/1365914?v=4?s=100" width="100px;" alt="Liam Lowsley-Williams"/><br /><sub><b>Liam Lowsley-Williams</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liamlows"><img src="https://avatars.githubusercontent.com/u/1365914?v=4?s=100" width="100px;" alt="Liam Lowsley-Williams"/><br /><sub><b>Liam Lowsley-Williams</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/oes-rioniz/"><img src="https://avatars.githubusercontent.com/u/5172296?v=4?s=100" width="100px;" alt="Omid"/><br /><sub><b>Omid</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kathari00"><img src="https://avatars.githubusercontent.com/u/42547712?v=4?s=100" width="100px;" alt="Katharina Eiserfey"/><br /><sub><b>Katharina Eiserfey</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luca-peruzzo"><img src="https://avatars.githubusercontent.com/u/69015314?v=4?s=100" width="100px;" alt="Luca Peruzzo"/><br /><sub><b>Luca Peruzzo</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nima70"><img src="https://avatars.githubusercontent.com/u/5094767?v=4?s=100" width="100px;" alt="Nima Shokouhfar"/><br /><sub><b>Nima Shokouhfar</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://mruder.dev"><img src="https://avatars.githubusercontent.com/u/18495294?v=4?s=100" width="100px;" alt="Marvin A. Ruder"/><br /><sub><b>Marvin A. Ruder</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/issues?q=author%3Amarvinruder" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvn2060"><img src="https://avatars.githubusercontent.com/u/45450852?v=4?s=100" width="100px;" alt="HI_OuO"/><br /><sub><b>HI_OuO</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zvn2060" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tripheo0412"><img src="https://avatars.githubusercontent.com/u/25382052?v=4?s=100" width="100px;" alt="Tri Hoang"/><br /><sub><b>Tri Hoang</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=tripheo0412" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://t.me/AAT_L"><img src="https://avatars.githubusercontent.com/u/118743608?v=4?s=100" width="100px;" alt="Lesha"/><br /><sub><b>Lesha</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=EternalSide" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.bacongobbler.com"><img src="https://avatars.githubusercontent.com/u/1360539?v=4?s=100" width="100px;" alt="Matthew Fisher"/><br /><sub><b>Matthew Fisher</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=bacongobbler" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kodebach"><img src="https://avatars.githubusercontent.com/u/23529132?v=4?s=100" width="100px;" alt="Klemens Böswirth"/><br /><sub><b>Klemens Böswirth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kodebach" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wnmzzzz"><img src="https://avatars.githubusercontent.com/u/117174301?v=4?s=100" width="100px;" alt="wnmzzzz"/><br /><sub><b>wnmzzzz</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=wnmzzzz" title="Tests">⚠️</a></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
35
package.json
35
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "11.0.0",
|
"version": "11.8.23",
|
||||||
"description": "Framework to create custom Keycloak UIs",
|
"description": "Framework to create custom Keycloak UIs",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -38,12 +38,14 @@
|
|||||||
"dist/",
|
"dist/",
|
||||||
"!dist/tsconfig.tsbuildinfo",
|
"!dist/tsconfig.tsbuildinfo",
|
||||||
"!dist/bin/",
|
"!dist/bin/",
|
||||||
|
"dist/bin/**/*.d.ts",
|
||||||
"dist/bin/main.js",
|
"dist/bin/main.js",
|
||||||
"dist/bin/*.index.js",
|
"dist/bin/*.index.js",
|
||||||
"dist/bin/*.node",
|
"dist/bin/*.node",
|
||||||
"dist/bin/shared/constants.js",
|
"dist/bin/shared/constants.js",
|
||||||
"dist/bin/shared/*.d.ts",
|
"dist/bin/shared/constants.js.map",
|
||||||
"dist/bin/shared/*.js.map",
|
"dist/bin/shared/customHandler.js",
|
||||||
|
"dist/bin/shared/customHandler.js.map",
|
||||||
"!dist/vite-plugin/",
|
"!dist/vite-plugin/",
|
||||||
"dist/vite-plugin/index.js",
|
"dist/vite-plugin/index.js",
|
||||||
"dist/vite-plugin/index.d.ts",
|
"dist/vite-plugin/index.d.ts",
|
||||||
@ -62,12 +64,13 @@
|
|||||||
],
|
],
|
||||||
"homepage": "https://www.keycloakify.dev",
|
"homepage": "https://www.keycloakify.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tsafe": "^1.6.6"
|
"tsafe": "^1.8.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.5",
|
"@babel/core": "^7.24.5",
|
||||||
"@babel/generator": "^7.24.5",
|
"@babel/generator": "^7.24.5",
|
||||||
"@babel/parser": "^7.24.5",
|
"@babel/parser": "^7.24.5",
|
||||||
|
"@babel/preset-env": "7.24.8",
|
||||||
"@babel/types": "^7.24.5",
|
"@babel/types": "^7.24.5",
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@octokit/rest": "^20.1.1",
|
"@octokit/rest": "^20.1.1",
|
||||||
@ -75,24 +78,31 @@
|
|||||||
"@storybook/builder-webpack5": "^6.5.13",
|
"@storybook/builder-webpack5": "^6.5.13",
|
||||||
"@storybook/manager-webpack5": "^6.5.13",
|
"@storybook/manager-webpack5": "^6.5.13",
|
||||||
"@storybook/react": "^6.5.13",
|
"@storybook/react": "^6.5.13",
|
||||||
"eslint-plugin-storybook": "^0.6.7",
|
|
||||||
"@types/babel__generator": "^7.6.4",
|
"@types/babel__generator": "^7.6.4",
|
||||||
|
"@types/dompurify": "^2.0.0",
|
||||||
"@types/make-fetch-happen": "^10.0.1",
|
"@types/make-fetch-happen": "^10.0.1",
|
||||||
"@types/minimist": "^1.2.2",
|
"@types/minimist": "^1.2.2",
|
||||||
"@types/node": "^18.15.3",
|
"@types/node": "^18.15.3",
|
||||||
|
"@types/properties-parser": "^0.3.3",
|
||||||
"@types/react": "^18.0.35",
|
"@types/react": "^18.0.35",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/yauzl": "^2.10.3",
|
"@types/yauzl": "^2.10.3",
|
||||||
"@vercel/ncc": "^0.38.1",
|
"@vercel/ncc": "^0.38.1",
|
||||||
|
"babel-loader": "9.1.3",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cheerio": "1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"chokidar-cli": "^3.0.0",
|
"chokidar-cli": "^3.0.0",
|
||||||
"cli-select": "^1.1.2",
|
"cli-select": "^1.1.2",
|
||||||
|
"dompurify": "^3.1.6",
|
||||||
|
"eslint-plugin-storybook": "^0.6.7",
|
||||||
|
"evt": "^2.5.8",
|
||||||
|
"html-entities": "^2.5.2",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
|
"isomorphic-dompurify": "^2.15.0",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
"magic-string": "^0.30.7",
|
"magic-string": "^0.30.7",
|
||||||
"make-fetch-happen": "^11.0.3",
|
"make-fetch-happen": "^11.0.3",
|
||||||
"powerhooks": "^1.0.10",
|
"powerhooks": "^1.0.19",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"properties-parser": "^0.3.1",
|
"properties-parser": "^0.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -103,20 +113,13 @@
|
|||||||
"termost": "^v0.12.1",
|
"termost": "^v0.12.1",
|
||||||
"tsc-alias": "^1.8.10",
|
"tsc-alias": "^1.8.10",
|
||||||
"tss-react": "^4.9.10",
|
"tss-react": "^4.9.10",
|
||||||
|
"tsx": "^4.15.5",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"yauzl": "^2.10.0",
|
|
||||||
"zod": "^3.17.10",
|
|
||||||
"evt": "^2.5.7",
|
|
||||||
"tsx": "^4.15.5",
|
|
||||||
"html-entities": "^2.5.2",
|
|
||||||
"isomorphic-dompurify": "^2.15.0",
|
|
||||||
"dompurify": "^3.1.6",
|
|
||||||
"@types/dompurify": "^2.0.0",
|
|
||||||
"webpack": "5.93.0",
|
"webpack": "5.93.0",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"babel-loader": "9.1.3",
|
"yauzl": "^2.10.0",
|
||||||
"@babel/preset-env": "7.24.8"
|
"zod": "^3.17.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
scripts/build/downloadKeycloakifyLogging.ts
Normal file
39
scripts/build/downloadKeycloakifyLogging.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtractArchive";
|
||||||
|
import { cacheDirPath } from "../shared/cacheDirPath";
|
||||||
|
import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||||
|
import { existsAsync } from "../../src/bin/tools/fs.existsAsync";
|
||||||
|
import * as fs from "fs/promises";
|
||||||
|
import {
|
||||||
|
KEYCLOAKIFY_LOGGING_VERSION,
|
||||||
|
KEYCLOAKIFY_LOGIN_JAR_BASENAME
|
||||||
|
} from "../../src/bin/shared/constants";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
|
||||||
|
export async function downloadKeycloakifyLogging(params: { distDirPath: string }) {
|
||||||
|
const { distDirPath } = params;
|
||||||
|
|
||||||
|
const jarFilePath = pathJoin(
|
||||||
|
distDirPath,
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
"start-keycloak",
|
||||||
|
KEYCLOAKIFY_LOGIN_JAR_BASENAME
|
||||||
|
);
|
||||||
|
|
||||||
|
if (await existsAsync(jarFilePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { archiveFilePath } = await downloadAndExtractArchive({
|
||||||
|
cacheDirPath,
|
||||||
|
fetchOptions: getProxyFetchOptions({
|
||||||
|
npmConfigGetCwd: getThisCodebaseRootDirPath()
|
||||||
|
}),
|
||||||
|
url: `https://github.com/keycloakify/keycloakify-logging/releases/download/${KEYCLOAKIFY_LOGGING_VERSION}/keycloakify-logging-${KEYCLOAKIFY_LOGGING_VERSION}.jar`,
|
||||||
|
uniqueIdOfOnArchiveFile: "no extraction",
|
||||||
|
onArchiveFile: async () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.cp(archiveFilePath, jarFilePath);
|
||||||
|
}
|
@ -7,6 +7,7 @@ import { createAccountV1Dir } from "./createAccountV1Dir";
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { run } from "../shared/run";
|
import { run } from "../shared/run";
|
||||||
import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
||||||
|
import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
console.log(chalk.cyan("Building Keycloakify..."));
|
console.log(chalk.cyan("Building Keycloakify..."));
|
||||||
@ -40,7 +41,9 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`);
|
run(
|
||||||
|
`npx ncc build ${join("dist", "bin", "main.js")} --external prettier -o ${join("dist", "ncc_out")}`
|
||||||
|
);
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
srcDirPath: join("dist", "ncc_out"),
|
srcDirPath: join("dist", "ncc_out"),
|
||||||
@ -113,7 +116,7 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
|||||||
}
|
}
|
||||||
|
|
||||||
run(
|
run(
|
||||||
`npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join(
|
`npx ncc build ${join("dist", "vite-plugin", "index.js")} --external prettier -o ${join(
|
||||||
"dist",
|
"dist",
|
||||||
"ncc_out"
|
"ncc_out"
|
||||||
)}`
|
)}`
|
||||||
@ -146,9 +149,6 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
|||||||
fs.cpSync(dirBasename, destDirPath, { recursive: true });
|
fs.cpSync(dirBasename, destDirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await createPublicKeycloakifyDevResourcesDir();
|
|
||||||
await createAccountV1Dir();
|
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
srcDirPath: join("stories"),
|
srcDirPath: join("stories"),
|
||||||
destDirPath: join("dist", "stories"),
|
destDirPath: join("dist", "stories"),
|
||||||
@ -161,6 +161,12 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await createPublicKeycloakifyDevResourcesDir();
|
||||||
|
await createAccountV1Dir();
|
||||||
|
await downloadKeycloakifyLogging({
|
||||||
|
distDirPath: join(process.cwd(), "dist")
|
||||||
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
|
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import {
|
import { join as pathJoin, basename as pathBasename, dirname as pathDirname } from "path";
|
||||||
join as pathJoin,
|
|
||||||
relative as pathRelative,
|
|
||||||
basename as pathBasename,
|
|
||||||
dirname as pathDirname
|
|
||||||
} from "path";
|
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { run } from "../shared/run";
|
import { run } from "../shared/run";
|
||||||
import { cacheDirPath as cacheDirPath_base } from "../shared/cacheDirPath";
|
import { cacheDirPath as cacheDirPath_base } from "../shared/cacheDirPath";
|
||||||
@ -41,13 +36,12 @@ export function vendorFrontendDependencies(params: { distDirPath: string }) {
|
|||||||
webpackConfigJsFilePath,
|
webpackConfigJsFilePath,
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
[
|
[
|
||||||
`const path = require('path');`,
|
|
||||||
``,
|
``,
|
||||||
`module.exports = {`,
|
`module.exports = {`,
|
||||||
` mode: 'production',`,
|
` mode: 'production',`,
|
||||||
` entry: '${filePath}',`,
|
` entry: Buffer.from("${Buffer.from(filePath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
|
||||||
` output: {`,
|
` output: {`,
|
||||||
` path: '${webpackOutputDirPath}',`,
|
` path: Buffer.from("${Buffer.from(webpackOutputDirPath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
|
||||||
` filename: '${pathBasename(webpackOutputFilePath)}',`,
|
` filename: '${pathBasename(webpackOutputFilePath)}',`,
|
||||||
` libraryTarget: 'module',`,
|
` libraryTarget: 'module',`,
|
||||||
` },`,
|
` },`,
|
||||||
@ -73,9 +67,9 @@ export function vendorFrontendDependencies(params: { distDirPath: string }) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
run(
|
run(`npx webpack --config ${pathBasename(webpackConfigJsFilePath)}`, {
|
||||||
`npx webpack --config ${pathRelative(process.cwd(), webpackConfigJsFilePath)}`
|
cwd: pathDirname(webpackConfigJsFilePath)
|
||||||
);
|
});
|
||||||
|
|
||||||
fs.readdirSync(webpackOutputDirPath)
|
fs.readdirSync(webpackOutputDirPath)
|
||||||
.filter(fileBasename => !fileBasename.endsWith(".txt"))
|
.filter(fileBasename => !fileBasename.endsWith(".txt"))
|
||||||
|
@ -1,66 +1,14 @@
|
|||||||
import { CONTAINER_NAME } from "../src/bin/shared/constants";
|
import { CONTAINER_NAME } from "../src/bin/shared/constants";
|
||||||
import child_process from "child_process";
|
import child_process from "child_process";
|
||||||
import { SemVer } from "../src/bin/tools/SemVer";
|
import { SemVer } from "../src/bin/tools/SemVer";
|
||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig";
|
||||||
|
import { cacheDirPath } from "./shared/cacheDirPath";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||||
|
import { writeRealmJsonFile } from "../src/bin/start-keycloak/realmConfig/ParsedRealmJson";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { Deferred } from "evt/tools/Deferred";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import { is } from "tsafe/is";
|
|
||||||
import { run } from "./shared/run";
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
{
|
|
||||||
const dCompleted = new Deferred<void>();
|
|
||||||
|
|
||||||
const child = child_process.spawn(
|
|
||||||
"docker",
|
|
||||||
[
|
|
||||||
...["exec", CONTAINER_NAME],
|
|
||||||
...["/opt/keycloak/bin/kc.sh", "export"],
|
|
||||||
...["--dir", "/tmp"],
|
|
||||||
...["--realm", "myrealm"],
|
|
||||||
...["--users", "realm_file"]
|
|
||||||
],
|
|
||||||
{ shell: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = "";
|
|
||||||
|
|
||||||
const onExit = (code: number | null) => {
|
|
||||||
dCompleted.reject(new Error(`Exited with code ${code}`));
|
|
||||||
};
|
|
||||||
|
|
||||||
child.on("exit", onExit);
|
|
||||||
|
|
||||||
child.stdout.on("data", data => {
|
|
||||||
const outputStr = data.toString("utf8");
|
|
||||||
|
|
||||||
if (outputStr.includes("Export finished successfully")) {
|
|
||||||
child.removeListener("exit", onExit);
|
|
||||||
|
|
||||||
child.kill();
|
|
||||||
|
|
||||||
dCompleted.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
output += outputStr;
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr.on("data", data => (output += chalk.red(data.toString("utf8"))));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dCompleted.pr;
|
|
||||||
} catch (error) {
|
|
||||||
assert(is<Error>(error));
|
|
||||||
|
|
||||||
console.log(chalk.red(error.message));
|
|
||||||
|
|
||||||
console.log(output);
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const keycloakMajorVersionNumber = SemVer.parse(
|
const keycloakMajorVersionNumber = SemVer.parse(
|
||||||
child_process
|
child_process
|
||||||
.execSync(`docker inspect --format '{{.Config.Image}}' ${CONTAINER_NAME}`)
|
.execSync(`docker inspect --format '{{.Config.Image}}' ${CONTAINER_NAME}`)
|
||||||
@ -69,19 +17,29 @@ import { run } from "./shared/run";
|
|||||||
.split(":")[1]
|
.split(":")[1]
|
||||||
).major;
|
).major;
|
||||||
|
|
||||||
const targetFilePath = pathRelative(
|
const parsedRealmJson = await dumpContainerConfig({
|
||||||
process.cwd(),
|
buildContext: {
|
||||||
pathJoin(
|
cacheDirPath
|
||||||
__dirname,
|
},
|
||||||
"..",
|
keycloakMajorVersionNumber,
|
||||||
"src",
|
realmName: "myrealm"
|
||||||
"bin",
|
});
|
||||||
"start-keycloak",
|
|
||||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
const realmJsonFilePath = pathJoin(
|
||||||
)
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
"start-keycloak",
|
||||||
|
"realmConfig",
|
||||||
|
"defaultConfig",
|
||||||
|
`realm-kc-${keycloakMajorVersionNumber}.json`
|
||||||
);
|
);
|
||||||
|
|
||||||
run(`docker cp ${CONTAINER_NAME}:/tmp/myrealm-realm.json ${targetFilePath}`);
|
await writeRealmJsonFile({
|
||||||
|
parsedRealmJson,
|
||||||
|
realmJsonFilePath,
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
|
console.log(chalk.green(`Realm config dumped to ${realmJsonFilePath}`));
|
||||||
})();
|
})();
|
||||||
|
@ -13,7 +13,7 @@ import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRoot
|
|||||||
import { deepAssign } from "../src/tools/deepAssign";
|
import { deepAssign } from "../src/tools/deepAssign";
|
||||||
import { THEME_TYPES } from "../src/bin/shared/constants";
|
import { THEME_TYPES } from "../src/bin/shared/constants";
|
||||||
import { transformCodebase } from "../src/bin/tools/transformCodebase";
|
import { transformCodebase } from "../src/bin/tools/transformCodebase";
|
||||||
const propertiesParser: any = require("properties-parser");
|
import propertiesParser from "properties-parser";
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
generateI18nMessages();
|
generateI18nMessages();
|
||||||
@ -37,7 +37,7 @@ async function generateI18nMessages() {
|
|||||||
|
|
||||||
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
||||||
|
|
||||||
for (const themeType of THEME_TYPES) {
|
for (const themeType of THEME_TYPES.filter(themeType => themeType !== "admin")) {
|
||||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
keycloakVersionId: (() => {
|
keycloakVersionId: (() => {
|
||||||
switch (themeType) {
|
switch (themeType) {
|
||||||
|
@ -45,7 +45,10 @@ const commonThirdPartyDeps = [
|
|||||||
.replace(/"!\.\/dist\//g, '"!./');
|
.replace(/"!\.\/dist\//g, '"!./');
|
||||||
|
|
||||||
modifiedPackageJsonContent = JSON.stringify(
|
modifiedPackageJsonContent = JSON.stringify(
|
||||||
{ ...JSON.parse(modifiedPackageJsonContent), version: "0.0.0" },
|
{
|
||||||
|
...JSON.parse(modifiedPackageJsonContent),
|
||||||
|
version: `0.0.0-rc.${~~(Math.random() * 1000000)}`
|
||||||
|
},
|
||||||
null,
|
null,
|
||||||
4
|
4
|
||||||
);
|
);
|
||||||
@ -55,7 +58,6 @@ const commonThirdPartyDeps = [
|
|||||||
Buffer.from(modifiedPackageJsonContent, "utf8")
|
Buffer.from(modifiedPackageJsonContent, "utf8")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
|
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
|
||||||
|
|
||||||
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
|
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
|
||||||
@ -64,6 +66,21 @@ fs.mkdirSync(yarnGlobalDirPath);
|
|||||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||||
const { targetModuleName, cwd } = params;
|
const { targetModuleName, cwd } = params;
|
||||||
|
|
||||||
|
if (targetModuleName === undefined) {
|
||||||
|
const packageJsonFilePath = pathJoin(cwd, "package.json");
|
||||||
|
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
delete packageJson["packageManager"];
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
packageJsonFilePath,
|
||||||
|
Buffer.from(JSON.stringify(packageJson, null, 2))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const cmd = [
|
const cmd = [
|
||||||
"yarn",
|
"yarn",
|
||||||
"link",
|
"link",
|
||||||
@ -77,7 +94,10 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
|||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
...(os.platform() === "win32"
|
...(os.platform() === "win32"
|
||||||
? { USERPROFILE: yarnGlobalDirPath }
|
? {
|
||||||
|
USERPROFILE: yarnGlobalDirPath,
|
||||||
|
LOCALAPPDATA: yarnGlobalDirPath
|
||||||
|
}
|
||||||
: { HOME: yarnGlobalDirPath })
|
: { HOME: yarnGlobalDirPath })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -108,7 +128,54 @@ if (testAppPaths.length === 0) {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath }));
|
testAppPaths.forEach(testAppPath => {
|
||||||
|
const packageJsonFilePath = pathJoin(testAppPath, "package.json");
|
||||||
|
|
||||||
|
const packageJsonContent = fs.readFileSync(packageJsonFilePath);
|
||||||
|
|
||||||
|
const parsedPackageJson = JSON.parse(packageJsonContent.toString("utf8")) as {
|
||||||
|
scripts?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let hasPostInstallOrPrepareScript = false;
|
||||||
|
|
||||||
|
if (parsedPackageJson.scripts !== undefined) {
|
||||||
|
for (const scriptName of ["postinstall", "prepare"]) {
|
||||||
|
if (parsedPackageJson.scripts[scriptName] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPostInstallOrPrepareScript = true;
|
||||||
|
|
||||||
|
delete parsedPackageJson.scripts[scriptName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPostInstallOrPrepareScript) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
packageJsonFilePath,
|
||||||
|
Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const restorePackageJson = () => {
|
||||||
|
if (!hasPostInstallOrPrepareScript) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(packageJsonFilePath, packageJsonContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync("yarn install", { cwd: testAppPath });
|
||||||
|
} catch (error) {
|
||||||
|
restorePackageJson();
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
restorePackageJson();
|
||||||
|
});
|
||||||
|
|
||||||
console.log("=== Linking common dependencies ===");
|
console.log("=== Linking common dependencies ===");
|
||||||
|
|
||||||
@ -155,4 +222,20 @@ testAppPaths.forEach(testAppPath =>
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testAppPaths.forEach(testAppPath => {
|
||||||
|
const { scripts = {} } = JSON.parse(
|
||||||
|
fs.readFileSync(pathJoin(testAppPath, "package.json")).toString("utf8")
|
||||||
|
) as {
|
||||||
|
scripts?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const scriptName of ["postinstall", "prepare"]) {
|
||||||
|
if (scripts[scriptName] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
execSync(`yarn run ${scriptName}`, { cwd: testAppPath });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
@ -1,49 +1,88 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join } from "path";
|
import { join as pathJoin, sep as pathSep } from "path";
|
||||||
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
|
||||||
import { crawl } from "../src/bin/tools/crawl";
|
|
||||||
import { run } from "./shared/run";
|
import { run } from "./shared/run";
|
||||||
|
import cliSelect from "cli-select";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { removeNodeModules } from "./tools/removeNodeModules";
|
||||||
|
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
||||||
|
|
||||||
{
|
(async () => {
|
||||||
const dirPath = "node_modules";
|
const parentDirPath = pathJoin(getThisCodebaseRootDirPath(), "..");
|
||||||
|
|
||||||
try {
|
const { starterName } = await (async () => {
|
||||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
const starterNames = fs
|
||||||
} catch {
|
.readdirSync(parentDirPath)
|
||||||
// NOTE: This is a workaround for windows
|
.filter(
|
||||||
// we can't remove locked executables.
|
basename =>
|
||||||
|
basename.includes("starter") &&
|
||||||
|
basename.includes("keycloakify") &&
|
||||||
|
fs.statSync(pathJoin(parentDirPath, basename)).isDirectory()
|
||||||
|
);
|
||||||
|
|
||||||
crawl({
|
if (starterNames.length === 0) {
|
||||||
dirPath,
|
console.log(
|
||||||
returnedPathsType: "absolute"
|
chalk.red(
|
||||||
}).forEach(filePath => {
|
`No starter found. Keycloakify Angular starter found in ${parentDirPath}`
|
||||||
try {
|
)
|
||||||
fs.rmSync(filePath, { force: true });
|
);
|
||||||
} catch (error) {
|
process.exit(-1);
|
||||||
if (filePath.endsWith(".exe")) {
|
}
|
||||||
return;
|
|
||||||
}
|
const starterName = await (async () => {
|
||||||
throw error;
|
if (starterNames.length === 1) {
|
||||||
|
return starterNames[0];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.rmSync("dist", { recursive: true, force: true });
|
console.log(chalk.cyan(`\nSelect a starter to link in:`));
|
||||||
fs.rmSync(".yarn_home", { recursive: true, force: true });
|
|
||||||
|
|
||||||
run("yarn install");
|
const { value } = await cliSelect<string>({
|
||||||
run("yarn build");
|
values: starterNames.map(starterName => `..${pathSep}${starterName}`)
|
||||||
|
}).catch(() => {
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
const starterName = "keycloakify-starter";
|
return value.split(pathSep)[1];
|
||||||
|
})();
|
||||||
|
|
||||||
fs.rmSync(join("..", starterName, "node_modules"), {
|
return { starterName };
|
||||||
recursive: true,
|
})();
|
||||||
force: true
|
|
||||||
});
|
|
||||||
|
|
||||||
run("yarn install", { cwd: join("..", starterName) });
|
const startTime = Date.now();
|
||||||
|
|
||||||
run(`npx tsx ${join("scripts", "link-in-app.ts")} ${starterName}`);
|
console.log(chalk.cyan(`\n\nLinking in ..${pathSep}${starterName}...`));
|
||||||
|
|
||||||
startRebuildOnSrcChange();
|
removeNodeModules({
|
||||||
|
nodeModulesDirPath: pathJoin(getThisCodebaseRootDirPath(), "node_modules")
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.rmSync(pathJoin(getThisCodebaseRootDirPath(), "dist"), {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
fs.rmSync(pathJoin(getThisCodebaseRootDirPath(), ".yarn_home"), {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
|
||||||
|
run("yarn install");
|
||||||
|
run("yarn build");
|
||||||
|
|
||||||
|
const starterDirPath = pathJoin(parentDirPath, starterName);
|
||||||
|
|
||||||
|
removeNodeModules({
|
||||||
|
nodeModulesDirPath: pathJoin(starterDirPath, "node_modules")
|
||||||
|
});
|
||||||
|
|
||||||
|
run("yarn install", { cwd: starterDirPath });
|
||||||
|
|
||||||
|
run(`npx tsx ${pathJoin("scripts", "link-in-app.ts")} ${starterName}`);
|
||||||
|
|
||||||
|
const durationSeconds = Math.round((Date.now() - startTime) / 1000);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
startRebuildOnSrcChange();
|
||||||
|
|
||||||
|
console.log(chalk.green(`\n\nLinked in ${starterName} in ${durationSeconds}s`));
|
||||||
|
})();
|
||||||
|
@ -280,6 +280,24 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
"fonts",
|
"fonts",
|
||||||
"OpenSans-Semibold-webfont.woff2"
|
"OpenSans-Semibold-webfont.woff2"
|
||||||
),
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.woff2"
|
||||||
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.woff"
|
||||||
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.ttf"
|
||||||
|
),
|
||||||
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
||||||
pathJoin("jquery", "dist", "jquery.min.js"),
|
pathJoin("jquery", "dist", "jquery.min.js"),
|
||||||
pathJoin("rfc4648", "lib", "rfc4648.js")
|
pathJoin("rfc4648", "lib", "rfc4648.js")
|
||||||
|
27
scripts/tools/removeNodeModules.ts
Normal file
27
scripts/tools/removeNodeModules.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { crawl } from "../../src/bin/tools/crawl";
|
||||||
|
|
||||||
|
export function removeNodeModules(params: { nodeModulesDirPath: string }) {
|
||||||
|
const { nodeModulesDirPath } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.rmSync(nodeModulesDirPath, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// NOTE: This is a workaround for windows
|
||||||
|
// we can't remove locked executables.
|
||||||
|
|
||||||
|
crawl({
|
||||||
|
dirPath: nodeModulesDirPath,
|
||||||
|
returnedPathsType: "absolute"
|
||||||
|
}).forEach(filePath => {
|
||||||
|
try {
|
||||||
|
fs.rmSync(filePath, { force: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (filePath.endsWith(".exe")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import type { ClassKey } from "keycloakify/account/lib/kcClsx";
|
||||||
|
|
||||||
export type TemplateProps<KcContext, I18n> = {
|
export type TemplateProps<KcContext, I18n> = {
|
||||||
kcContext: KcContext;
|
kcContext: KcContext;
|
||||||
@ -10,17 +11,4 @@ export type TemplateProps<KcContext, I18n> = {
|
|||||||
active: string;
|
active: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClassKey =
|
export type { ClassKey };
|
||||||
| "kcHtmlClass"
|
|
||||||
| "kcBodyClass"
|
|
||||||
| "kcButtonClass"
|
|
||||||
| "kcButtonPrimaryClass"
|
|
||||||
| "kcButtonLargeClass"
|
|
||||||
| "kcButtonDefaultClass"
|
|
||||||
| "kcContentWrapperClass"
|
|
||||||
| "kcFormClass"
|
|
||||||
| "kcFormGroupClass"
|
|
||||||
| "kcInputWrapperClass"
|
|
||||||
| "kcLabelClass"
|
|
||||||
| "kcInputClass"
|
|
||||||
| "kcInputErrorMessageClass";
|
|
||||||
|
@ -1,5 +1,19 @@
|
|||||||
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
|
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
|
||||||
import type { ClassKey } from "keycloakify/account/TemplateProps";
|
|
||||||
|
export type ClassKey =
|
||||||
|
| "kcHtmlClass"
|
||||||
|
| "kcBodyClass"
|
||||||
|
| "kcButtonClass"
|
||||||
|
| "kcButtonPrimaryClass"
|
||||||
|
| "kcButtonLargeClass"
|
||||||
|
| "kcButtonDefaultClass"
|
||||||
|
| "kcContentWrapperClass"
|
||||||
|
| "kcFormClass"
|
||||||
|
| "kcFormGroupClass"
|
||||||
|
| "kcInputWrapperClass"
|
||||||
|
| "kcLabelClass"
|
||||||
|
| "kcInputClass"
|
||||||
|
| "kcInputErrorMessageClass";
|
||||||
|
|
||||||
export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
||||||
defaultClasses: {
|
defaultClasses: {
|
||||||
@ -20,6 +34,4 @@ export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export type { ClassKey };
|
|
||||||
|
|
||||||
export type KcClsx = ReturnType<typeof getKcClsx>["kcClsx"];
|
export type KcClsx = ReturnType<typeof getKcClsx>["kcClsx"];
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { type TemplateProps, type ClassKey } from "keycloakify/account/TemplateProps";
|
import { type TemplateProps, type ClassKey } from "keycloakify/account/TemplateProps";
|
||||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||||
|
|
||||||
|
@ -5,25 +5,30 @@ import {
|
|||||||
ACCOUNT_THEME_PAGE_IDS,
|
ACCOUNT_THEME_PAGE_IDS,
|
||||||
type LoginThemePageId,
|
type LoginThemePageId,
|
||||||
type AccountThemePageId,
|
type AccountThemePageId,
|
||||||
THEME_TYPES,
|
THEME_TYPES
|
||||||
type ThemeType
|
|
||||||
} from "./shared/constants";
|
} from "./shared/constants";
|
||||||
import { capitalize } from "tsafe/capitalize";
|
import { capitalize } from "tsafe/capitalize";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||||
import { assert, Equals } from "tsafe/assert";
|
import { assert, Equals } from "tsafe/assert";
|
||||||
import type { CliCommandOptions } from "./main";
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
import { getBuildContext } from "./shared/buildContext";
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||||
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { cliCommandOptions } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const buildContext = getBuildContext({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
cliCommandOptions
|
commandName: "add-story",
|
||||||
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasBeenHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(chalk.cyan("Theme type:"));
|
console.log(chalk.cyan("Theme type:"));
|
||||||
|
|
||||||
const themeType = await (async () => {
|
const themeType = await (async () => {
|
||||||
@ -33,6 +38,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
case "login":
|
case "login":
|
||||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
|
case "admin":
|
||||||
|
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
});
|
});
|
||||||
@ -43,7 +50,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
return values[0];
|
return values[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { value } = await cliSelect<ThemeType>({
|
const { value } = await cliSelect({
|
||||||
values
|
values
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
@ -62,6 +69,16 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
);
|
);
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeType === "admin") {
|
||||||
|
console.log(
|
||||||
|
`${chalk.red("✗")} Sorry, there is no Storybook support for the Admin UI.`
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`→ ${themeType}`);
|
console.log(`→ ${themeType}`);
|
||||||
@ -102,7 +119,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentCode = fs
|
let sourceCode = fs
|
||||||
.readFileSync(
|
.readFileSync(
|
||||||
pathJoin(
|
pathJoin(
|
||||||
getThisCodebaseRootDirPath(),
|
getThisCodebaseRootDirPath(),
|
||||||
@ -116,6 +133,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
.replace('import React from "react";\n', "")
|
.replace('import React from "react";\n', "")
|
||||||
.replace(/from "[./]+dist\//, 'from "keycloakify/');
|
.replace(/from "[./]+dist\//, 'from "keycloakify/');
|
||||||
|
|
||||||
|
run_prettier: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break run_prettier;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
filePath: targetFilePath,
|
||||||
|
sourceCode: sourceCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const targetDirPath = pathDirname(targetFilePath);
|
const targetDirPath = pathDirname(targetFilePath);
|
||||||
|
|
||||||
@ -124,7 +152,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
fs.writeFileSync(targetFilePath, Buffer.from(sourceCode, "utf8"));
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
|
@ -1,13 +1,96 @@
|
|||||||
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
import { getBuildContext } from "./shared/buildContext";
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
import type { CliCommandOptions } from "./main";
|
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants";
|
||||||
|
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { rmSync } from "./tools/fs.rmSync";
|
||||||
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
|
import { transformCodebase } from "./tools/transformCodebase";
|
||||||
|
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { cliCommandOptions } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const buildContext = getBuildContext({ cliCommandOptions });
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
|
commandName: "copy-keycloak-resources-to-public",
|
||||||
copyKeycloakResourcesToPublic({
|
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasBeenHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const destDirPath = pathJoin(
|
||||||
|
buildContext.publicDirPath,
|
||||||
|
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
||||||
|
);
|
||||||
|
|
||||||
|
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
|
||||||
|
|
||||||
|
const keycloakifyBuildinfoRaw = JSON.stringify(
|
||||||
|
{
|
||||||
|
keycloakifyVersion: readThisNpmPackageVersion()
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
skip_if_already_done: {
|
||||||
|
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
|
||||||
|
break skip_if_already_done;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keycloakifyBuildinfoRaw_previousRun = fs
|
||||||
|
.readFileSync(keycloakifyBuildinfoFilePath)
|
||||||
|
.toString("utf8");
|
||||||
|
|
||||||
|
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
|
||||||
|
break skip_if_already_done;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rmSync(destDirPath, { force: true, recursive: true });
|
||||||
|
|
||||||
|
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
|
||||||
|
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
|
||||||
|
force: true,
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
|
||||||
|
force: true,
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.mkdirSync(destDirPath, { recursive: true });
|
||||||
|
|
||||||
|
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"res",
|
||||||
|
"public",
|
||||||
|
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
||||||
|
),
|
||||||
|
destDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(destDirPath, "README.txt"),
|
||||||
|
Buffer.from(
|
||||||
|
// prettier-ignore
|
||||||
|
[
|
||||||
|
"This directory is only used in dev mode by Keycloakify",
|
||||||
|
"It won't be included in your final build.",
|
||||||
|
"Do not modify anything in this directory.",
|
||||||
|
].join("\n")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
keycloakifyBuildinfoFilePath,
|
||||||
|
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,30 +7,30 @@ import {
|
|||||||
ACCOUNT_THEME_PAGE_IDS,
|
ACCOUNT_THEME_PAGE_IDS,
|
||||||
type LoginThemePageId,
|
type LoginThemePageId,
|
||||||
type AccountThemePageId,
|
type AccountThemePageId,
|
||||||
THEME_TYPES,
|
THEME_TYPES
|
||||||
type ThemeType
|
|
||||||
} from "./shared/constants";
|
} from "./shared/constants";
|
||||||
import { capitalize } from "tsafe/capitalize";
|
import { capitalize } from "tsafe/capitalize";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import {
|
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||||
join as pathJoin,
|
|
||||||
relative as pathRelative,
|
|
||||||
dirname as pathDirname,
|
|
||||||
basename as pathBasename
|
|
||||||
} from "path";
|
|
||||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||||
import { assert, Equals } from "tsafe/assert";
|
import { assert, Equals } from "tsafe/assert";
|
||||||
import type { CliCommandOptions } from "./main";
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
import { getBuildContext } from "./shared/buildContext";
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
|
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { cliCommandOptions } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const buildContext = getBuildContext({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
cliCommandOptions
|
commandName: "eject-page",
|
||||||
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasBeenHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(chalk.cyan("Theme type:"));
|
console.log(chalk.cyan("Theme type:"));
|
||||||
|
|
||||||
const themeType = await (async () => {
|
const themeType = await (async () => {
|
||||||
@ -40,6 +40,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
case "login":
|
case "login":
|
||||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
|
case "admin":
|
||||||
|
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
});
|
});
|
||||||
@ -50,7 +52,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
return values[0];
|
return values[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { value } = await cliSelect<ThemeType>({
|
const { value } = await cliSelect({
|
||||||
values
|
values
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
@ -59,87 +61,28 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
return value;
|
return value;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
if (themeType === "admin") {
|
||||||
|
console.log("Use `npx keycloakify own` command instead, see documentation");
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
themeType === "account" &&
|
themeType === "account" &&
|
||||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||||
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
||||||
) {
|
) {
|
||||||
const srcDirPath = pathJoin(
|
|
||||||
pathDirname(buildContext.packageJsonFilePath),
|
|
||||||
"node_modules",
|
|
||||||
"@keycloakify",
|
|
||||||
"keycloak-account-ui",
|
|
||||||
"src"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
[
|
chalk.yellow(
|
||||||
`There isn't an interactive CLI to eject components of the Single-Page Account theme.`,
|
[
|
||||||
`You can however copy paste into your codebase the any file or directory from the following source directory:`,
|
"You are implementing a Single-Page Account theme.",
|
||||||
``,
|
"The eject-page command isn't applicable in this context"
|
||||||
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
].join("\n")
|
||||||
``
|
)
|
||||||
].join("\n")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
eject_entrypoint: {
|
process.exit(1);
|
||||||
const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx";
|
return;
|
||||||
|
|
||||||
const accountThemeSrcDirPath = pathJoin(
|
|
||||||
buildContext.themeSrcDirPath,
|
|
||||||
"account"
|
|
||||||
);
|
|
||||||
|
|
||||||
const targetFilePath = pathJoin(
|
|
||||||
accountThemeSrcDirPath,
|
|
||||||
kcAccountUiTsxFileRelativePath
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fs.existsSync(targetFilePath)) {
|
|
||||||
break eject_entrypoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.cpSync(
|
|
||||||
pathJoin(srcDirPath, kcAccountUiTsxFileRelativePath),
|
|
||||||
targetFilePath
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
|
||||||
const kcPageTsxFilePath = pathJoin(accountThemeSrcDirPath, "KcPage.tsx");
|
|
||||||
|
|
||||||
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
|
|
||||||
|
|
||||||
const componentName = pathBasename(
|
|
||||||
kcAccountUiTsxFileRelativePath
|
|
||||||
).replace(/.tsx$/, "");
|
|
||||||
|
|
||||||
const modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
|
||||||
`@keycloakify/keycloak-account-ui/${componentName}`,
|
|
||||||
`./${componentName}`
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
kcPageTsxFilePath,
|
|
||||||
Buffer.from(modifiedKcPageTsxCode, "utf8")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const routesTsxFilePath = pathRelative(
|
|
||||||
process.cwd(),
|
|
||||||
pathJoin(srcDirPath, "routes.tsx")
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
`To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`,
|
|
||||||
`The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
|
|
||||||
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), accountThemeSrcDirPath)}\``,
|
|
||||||
`then update the import of routes in ${kcAccountUiTsxFileRelativePath}.`
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`→ ${themeType}`);
|
console.log(`→ ${themeType}`);
|
||||||
@ -149,12 +92,14 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
const templateValue = "Template.tsx (Layout common to every page)";
|
const templateValue = "Template.tsx (Layout common to every page)";
|
||||||
const userProfileFormFieldsValue =
|
const userProfileFormFieldsValue =
|
||||||
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
|
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
|
||||||
|
const otherPageValue = "The page you're looking for isn't listed here";
|
||||||
|
|
||||||
const { value: pageIdOrComponent } = await cliSelect<
|
const { value: pageIdOrComponent } = await cliSelect<
|
||||||
| LoginThemePageId
|
| LoginThemePageId
|
||||||
| AccountThemePageId
|
| AccountThemePageId
|
||||||
| typeof templateValue
|
| typeof templateValue
|
||||||
| typeof userProfileFormFieldsValue
|
| typeof userProfileFormFieldsValue
|
||||||
|
| typeof otherPageValue
|
||||||
>({
|
>({
|
||||||
values: (() => {
|
values: (() => {
|
||||||
switch (themeType) {
|
switch (themeType) {
|
||||||
@ -162,10 +107,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
return [
|
return [
|
||||||
templateValue,
|
templateValue,
|
||||||
userProfileFormFieldsValue,
|
userProfileFormFieldsValue,
|
||||||
...LOGIN_THEME_PAGE_IDS
|
...LOGIN_THEME_PAGE_IDS,
|
||||||
|
otherPageValue
|
||||||
];
|
];
|
||||||
case "account":
|
case "account":
|
||||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS];
|
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS, otherPageValue];
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
})()
|
})()
|
||||||
@ -173,6 +119,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (pageIdOrComponent === otherPageValue) {
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
"To style a page not included in the base Keycloak, such as one added by a third-party Keycloak extension,",
|
||||||
|
"refer to the documentation: https://docs.keycloakify.dev/features/styling-a-custom-page-not-included-in-base-keycloak"
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`→ ${pageIdOrComponent}`);
|
console.log(`→ ${pageIdOrComponent}`);
|
||||||
|
|
||||||
const componentBasename = (() => {
|
const componentBasename = (() => {
|
||||||
@ -216,7 +173,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentCode = fs
|
let componentCode = fs
|
||||||
.readFileSync(
|
.readFileSync(
|
||||||
pathJoin(
|
pathJoin(
|
||||||
getThisCodebaseRootDirPath(),
|
getThisCodebaseRootDirPath(),
|
||||||
@ -228,6 +185,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
)
|
)
|
||||||
.toString("utf8");
|
.toString("utf8");
|
||||||
|
|
||||||
|
run_prettier: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break run_prettier;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentCode = await runPrettier({
|
||||||
|
filePath: targetFilePath,
|
||||||
|
sourceCode: componentCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const targetDirPath = pathDirname(targetFilePath);
|
const targetDirPath = pathDirname(targetFilePath);
|
||||||
|
|
||||||
@ -244,12 +212,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
)} copy pasted from the Keycloakify source code into your project`
|
)} copy pasted from the Keycloakify source code into your project`
|
||||||
);
|
);
|
||||||
|
|
||||||
edit_KcApp: {
|
edit_KcPage: {
|
||||||
if (
|
if (
|
||||||
pageIdOrComponent !== templateValue &&
|
pageIdOrComponent !== templateValue &&
|
||||||
pageIdOrComponent !== userProfileFormFieldsValue
|
pageIdOrComponent !== userProfileFormFieldsValue
|
||||||
) {
|
) {
|
||||||
break edit_KcApp;
|
break edit_KcPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const kcAppTsxPath = pathJoin(
|
const kcAppTsxPath = pathJoin(
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import { join as pathJoin } from "path";
|
|
||||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
|
||||||
|
|
||||||
export function copyBoilerplate(params: {
|
|
||||||
accountThemeType: "Single-Page" | "Multi-Page";
|
|
||||||
accountThemeSrcDirPath: string;
|
|
||||||
}) {
|
|
||||||
const { accountThemeType, accountThemeSrcDirPath } = params;
|
|
||||||
|
|
||||||
fs.cpSync(
|
|
||||||
pathJoin(
|
|
||||||
getThisCodebaseRootDirPath(),
|
|
||||||
"src",
|
|
||||||
"bin",
|
|
||||||
"initialize-account-theme",
|
|
||||||
"src",
|
|
||||||
(() => {
|
|
||||||
switch (accountThemeType) {
|
|
||||||
case "Single-Page":
|
|
||||||
return "single-page";
|
|
||||||
case "Multi-Page":
|
|
||||||
return "multi-page";
|
|
||||||
}
|
|
||||||
assert<Equals<typeof accountThemeType, never>>(false);
|
|
||||||
})()
|
|
||||||
),
|
|
||||||
accountThemeSrcDirPath,
|
|
||||||
{ recursive: true }
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,67 +1,31 @@
|
|||||||
import { getBuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import type { CliCommandOptions } from "../main";
|
|
||||||
import cliSelect from "cli-select";
|
import cliSelect from "cli-select";
|
||||||
import child_process from "child_process";
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
||||||
import { generateKcGenTs } from "../shared/generateKcGenTs";
|
import { command as updateKcGenCommand } from "../update-kc-gen";
|
||||||
|
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
||||||
|
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { cliCommandOptions } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const buildContext = getBuildContext({ cliCommandOptions });
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
|
commandName: "initialize-account-theme",
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasBeenHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
||||||
|
|
||||||
if (
|
exitIfUncommittedChanges({
|
||||||
fs.existsSync(accountThemeSrcDirPath) &&
|
projectDirPath: buildContext.projectDirPath
|
||||||
fs.readdirSync(accountThemeSrcDirPath).length > 0
|
});
|
||||||
) {
|
|
||||||
console.warn(
|
|
||||||
chalk.red(
|
|
||||||
`There is already a ${pathRelative(
|
|
||||||
process.cwd(),
|
|
||||||
accountThemeSrcDirPath
|
|
||||||
)} directory in your project. Aborting.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
exit_if_uncommitted_changes: {
|
|
||||||
let hasUncommittedChanges: boolean | undefined = undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
hasUncommittedChanges =
|
|
||||||
child_process
|
|
||||||
.execSync(`git status --porcelain`, {
|
|
||||||
cwd: buildContext.projectDirPath
|
|
||||||
})
|
|
||||||
.toString()
|
|
||||||
.trim() !== "";
|
|
||||||
} catch {
|
|
||||||
// Probably not a git repository
|
|
||||||
break exit_if_uncommitted_changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasUncommittedChanges) {
|
|
||||||
break exit_if_uncommitted_changes;
|
|
||||||
}
|
|
||||||
console.warn(
|
|
||||||
[
|
|
||||||
chalk.red(
|
|
||||||
"Please commit or stash your changes before running this command.\n"
|
|
||||||
),
|
|
||||||
"This command will modify your project's files so it's better to have a clean working directory",
|
|
||||||
"so that you can easily see what has been changed and revert if needed."
|
|
||||||
].join(" ")
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { value: accountThemeType } = await cliSelect({
|
const { value: accountThemeType } = await cliSelect({
|
||||||
values: ["Single-Page" as const, "Multi-Page" as const]
|
values: ["Single-Page" as const, "Multi-Page" as const]
|
||||||
@ -72,23 +36,41 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
switch (accountThemeType) {
|
switch (accountThemeType) {
|
||||||
case "Multi-Page":
|
case "Multi-Page":
|
||||||
{
|
{
|
||||||
const { initializeAccountTheme_multiPage } = await import(
|
if (
|
||||||
"./initializeAccountTheme_multiPage"
|
fs.existsSync(accountThemeSrcDirPath) &&
|
||||||
);
|
fs.readdirSync(accountThemeSrcDirPath).length > 0
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
chalk.red(
|
||||||
|
`There is already a ${pathRelative(
|
||||||
|
process.cwd(),
|
||||||
|
accountThemeSrcDirPath
|
||||||
|
)} directory in your project. Aborting.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await initializeAccountTheme_multiPage({
|
process.exit(-1);
|
||||||
accountThemeSrcDirPath
|
}
|
||||||
});
|
|
||||||
|
fs.cpSync(
|
||||||
|
pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
"initialize-account-theme",
|
||||||
|
"multi-page-boilerplate"
|
||||||
|
),
|
||||||
|
accountThemeSrcDirPath,
|
||||||
|
{ recursive: true }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "Single-Page":
|
case "Single-Page":
|
||||||
{
|
{
|
||||||
const { initializeAccountTheme_singlePage } = await import(
|
const { initializeSpa } = await import("../shared/initializeSpa");
|
||||||
"./initializeAccountTheme_singlePage"
|
|
||||||
);
|
|
||||||
|
|
||||||
await initializeAccountTheme_singlePage({
|
await initializeSpa({
|
||||||
accountThemeSrcDirPath,
|
themeType: "account",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -97,7 +79,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
|
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
|
||||||
|
|
||||||
await generateKcGenTs({
|
await updateKcGenCommand({
|
||||||
buildContext: {
|
buildContext: {
|
||||||
...buildContext,
|
...buildContext,
|
||||||
implementedThemeTypes: {
|
implementedThemeTypes: {
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { relative as pathRelative } from "path";
|
|
||||||
import chalk from "chalk";
|
|
||||||
import { copyBoilerplate } from "./copyBoilerplate";
|
|
||||||
|
|
||||||
export async function initializeAccountTheme_multiPage(params: {
|
|
||||||
accountThemeSrcDirPath: string;
|
|
||||||
}) {
|
|
||||||
const { accountThemeSrcDirPath } = params;
|
|
||||||
|
|
||||||
copyBoilerplate({
|
|
||||||
accountThemeType: "Multi-Page",
|
|
||||||
accountThemeSrcDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
chalk.green("The Multi-Page account theme has been initialized."),
|
|
||||||
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
|
||||||
import type { BuildContext } from "../shared/buildContext";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import chalk from "chalk";
|
|
||||||
import {
|
|
||||||
getLatestsSemVersionedTag,
|
|
||||||
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
|
||||||
} from "../shared/getLatestsSemVersionedTag";
|
|
||||||
import { SemVer } from "../tools/SemVer";
|
|
||||||
import fetch from "make-fetch-happen";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
|
||||||
import { is } from "tsafe/is";
|
|
||||||
import { id } from "tsafe/id";
|
|
||||||
import { npmInstall } from "../tools/npmInstall";
|
|
||||||
import { copyBoilerplate } from "./copyBoilerplate";
|
|
||||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
|
||||||
|
|
||||||
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
|
|
||||||
fetchOptions: BuildContext["fetchOptions"];
|
|
||||||
packageJsonFilePath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export async function initializeAccountTheme_singlePage(params: {
|
|
||||||
accountThemeSrcDirPath: string;
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
}) {
|
|
||||||
const { accountThemeSrcDirPath, buildContext } = params;
|
|
||||||
|
|
||||||
const OWNER = "keycloakify";
|
|
||||||
const REPO = "keycloak-account-ui";
|
|
||||||
|
|
||||||
const [semVersionedTag] = await getLatestsSemVersionedTag({
|
|
||||||
owner: OWNER,
|
|
||||||
repo: REPO,
|
|
||||||
count: 1,
|
|
||||||
doIgnoreReleaseCandidates: false,
|
|
||||||
buildContext
|
|
||||||
});
|
|
||||||
|
|
||||||
const dependencies = await fetch(
|
|
||||||
`https://raw.githubusercontent.com/${OWNER}/${REPO}/${semVersionedTag.tag}/dependencies.gen.json`,
|
|
||||||
buildContext.fetchOptions
|
|
||||||
)
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(
|
|
||||||
(() => {
|
|
||||||
type Dependencies = {
|
|
||||||
dependencies: Record<string, string>;
|
|
||||||
devDependencies?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const zDependencies = (() => {
|
|
||||||
type TargetType = Dependencies;
|
|
||||||
|
|
||||||
const zTargetType = z.object({
|
|
||||||
dependencies: z.record(z.string()),
|
|
||||||
devDependencies: z.record(z.string()).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
|
||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return o => zDependencies.parse(o);
|
|
||||||
})()
|
|
||||||
);
|
|
||||||
|
|
||||||
dependencies.dependencies["@keycloakify/keycloak-account-ui"] = SemVer.stringify(
|
|
||||||
semVersionedTag.version
|
|
||||||
);
|
|
||||||
|
|
||||||
const parsedPackageJson = (() => {
|
|
||||||
type ParsedPackageJson = {
|
|
||||||
dependencies?: Record<string, string>;
|
|
||||||
devDependencies?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const zParsedPackageJson = (() => {
|
|
||||||
type TargetType = ParsedPackageJson;
|
|
||||||
|
|
||||||
const zTargetType = z.object({
|
|
||||||
dependencies: z.record(z.string()).optional(),
|
|
||||||
devDependencies: z.record(z.string()).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
|
||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
|
||||||
})();
|
|
||||||
const parsedPackageJson = JSON.parse(
|
|
||||||
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
zParsedPackageJson.parse(parsedPackageJson);
|
|
||||||
|
|
||||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
|
||||||
|
|
||||||
return parsedPackageJson;
|
|
||||||
})();
|
|
||||||
|
|
||||||
parsedPackageJson.dependencies = {
|
|
||||||
...parsedPackageJson.dependencies,
|
|
||||||
...dependencies.dependencies
|
|
||||||
};
|
|
||||||
|
|
||||||
parsedPackageJson.devDependencies = {
|
|
||||||
...parsedPackageJson.devDependencies,
|
|
||||||
...dependencies.devDependencies
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Object.keys(parsedPackageJson.devDependencies).length === 0) {
|
|
||||||
delete parsedPackageJson.devDependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
buildContext.packageJsonFilePath,
|
|
||||||
JSON.stringify(parsedPackageJson, undefined, 4)
|
|
||||||
);
|
|
||||||
|
|
||||||
run_npm_install: {
|
|
||||||
if (
|
|
||||||
JSON.parse(
|
|
||||||
fs
|
|
||||||
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
|
|
||||||
.toString("utf8")
|
|
||||||
)["version"] === "0.0.0"
|
|
||||||
) {
|
|
||||||
//NOTE: Linked version
|
|
||||||
break run_npm_install;
|
|
||||||
}
|
|
||||||
|
|
||||||
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
|
|
||||||
}
|
|
||||||
|
|
||||||
copyBoilerplate({
|
|
||||||
accountThemeType: "Single-Page",
|
|
||||||
accountThemeSrcDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
chalk.green(
|
|
||||||
"The Single-Page account theme has been successfully initialized."
|
|
||||||
),
|
|
||||||
`Using Account UI of Keycloak version: ${chalk.bold(semVersionedTag.tag.split("-")[0])}`,
|
|
||||||
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`,
|
|
||||||
`Dependencies added to your project's package.json: `,
|
|
||||||
chalk.bold(JSON.stringify(dependencies, null, 2))
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { i18nBuilder } from "keycloakify/account";
|
||||||
|
import type { ThemeName } from "../kc.gen";
|
||||||
|
|
||||||
|
/** @see: https://docs.keycloakify.dev/features/i18n */
|
||||||
|
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
|
||||||
|
|
||||||
|
type I18n = typeof ofTypeI18n;
|
||||||
|
|
||||||
|
export { useI18n, type I18n };
|
@ -1,12 +0,0 @@
|
|||||||
import { i18nBuilder } from "keycloakify/account";
|
|
||||||
import type { ThemeName } from "../kc.gen";
|
|
||||||
|
|
||||||
const { useI18n, ofTypeI18n } = i18nBuilder
|
|
||||||
.withThemeName<ThemeName>()
|
|
||||||
.withExtraLanguages({})
|
|
||||||
.withCustomTranslations({})
|
|
||||||
.build();
|
|
||||||
|
|
||||||
type I18n = typeof ofTypeI18n;
|
|
||||||
|
|
||||||
export { useI18n, type I18n };
|
|
@ -1,7 +0,0 @@
|
|||||||
import type { KcContextLike } from "@keycloakify/keycloak-account-ui";
|
|
||||||
import type { KcEnvName } from "../kc.gen";
|
|
||||||
|
|
||||||
export type KcContext = KcContextLike & {
|
|
||||||
themeType: "account";
|
|
||||||
properties: Record<KcEnvName, string>;
|
|
||||||
};
|
|
@ -1,11 +0,0 @@
|
|||||||
import { lazy } from "react";
|
|
||||||
import { KcAccountUiLoader } from "@keycloakify/keycloak-account-ui";
|
|
||||||
import type { KcContext } from "./KcContext";
|
|
||||||
|
|
||||||
const KcAccountUi = lazy(() => import("@keycloakify/keycloak-account-ui/KcAccountUi"));
|
|
||||||
|
|
||||||
export default function KcPage(props: { kcContext: KcContext }) {
|
|
||||||
const { kcContext } = props;
|
|
||||||
|
|
||||||
return <KcAccountUiLoader kcContext={kcContext} KcAccountUi={KcAccountUi} />;
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals, is } from "tsafe/assert";
|
||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@ -8,12 +8,14 @@ import { id } from "tsafe/id";
|
|||||||
|
|
||||||
export type BuildContextLike = {
|
export type BuildContextLike = {
|
||||||
bundler: BuildContext["bundler"];
|
bundler: BuildContext["bundler"];
|
||||||
|
projectDirPath: string;
|
||||||
|
packageJsonFilePath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export function updateAccountThemeImplementationInConfig(params: {
|
export function updateAccountThemeImplementationInConfig(params: {
|
||||||
buildContext: BuildContext;
|
buildContext: BuildContextLike;
|
||||||
accountThemeType: "Single-Page" | "Multi-Page";
|
accountThemeType: "Single-Page" | "Multi-Page";
|
||||||
}) {
|
}) {
|
||||||
const { buildContext, accountThemeType } = params;
|
const { buildContext, accountThemeType } = params;
|
||||||
@ -81,6 +83,8 @@ export function updateAccountThemeImplementationInConfig(params: {
|
|||||||
|
|
||||||
zParsedPackageJson.parse(parsedPackageJson);
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
return parsedPackageJson;
|
return parsedPackageJson;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
39
src/bin/initialize-admin-theme.ts
Normal file
39
src/bin/initialize-admin-theme.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
|
import { initializeSpa } from "./shared/initializeSpa";
|
||||||
|
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
|
||||||
|
import { command as updateKcGenCommand } from "./update-kc-gen";
|
||||||
|
|
||||||
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
|
commandName: "initialize-admin-theme",
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasBeenHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exitIfUncommittedChanges({
|
||||||
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
await initializeSpa({
|
||||||
|
themeType: "admin",
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateKcGenCommand({
|
||||||
|
buildContext: {
|
||||||
|
...buildContext,
|
||||||
|
implementedThemeTypes: {
|
||||||
|
...buildContext.implementedThemeTypes,
|
||||||
|
admin: {
|
||||||
|
isImplemented: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -1,15 +1,34 @@
|
|||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
import { transformCodebase } from "./tools/transformCodebase";
|
import cliSelect from "cli-select";
|
||||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
import { getBuildContext } from "./shared/buildContext";
|
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
|
||||||
|
|
||||||
|
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import type { CliCommandOptions } from "./main";
|
import { assert, is, type Equals } from "tsafe/assert";
|
||||||
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
|
import { id } from "tsafe/id";
|
||||||
|
import { addSyncExtensionsToPostinstallScript } from "./shared/addSyncExtensionsToPostinstallScript";
|
||||||
|
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
|
||||||
|
import { npmInstall } from "./tools/npmInstall";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { z } from "zod";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { cliCommandOptions } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const buildContext = getBuildContext({ cliCommandOptions });
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
|
commandName: "initialize-email-theme",
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasBeenHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exitIfUncommittedChanges({
|
||||||
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||||
|
|
||||||
@ -18,69 +37,120 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
fs.readdirSync(emailThemeSrcDirPath).length > 0
|
fs.readdirSync(emailThemeSrcDirPath).length > 0
|
||||||
) {
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`There is already a non empty ${pathRelative(
|
chalk.red(
|
||||||
process.cwd(),
|
`There is already a ${pathRelative(
|
||||||
emailThemeSrcDirPath
|
process.cwd(),
|
||||||
)} directory in your project. Aborting.`
|
emailThemeSrcDirPath
|
||||||
|
)} directory in your project. Aborting.`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Initialize with the base email theme from which version of Keycloak?");
|
const { value: emailThemeType } = await cliSelect({
|
||||||
|
values: [
|
||||||
|
"native (FreeMarker)" as const,
|
||||||
|
"Another email templating solution" as const
|
||||||
|
]
|
||||||
|
}).catch(() => {
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
const { keycloakVersion } = await promptKeycloakVersion({
|
if (emailThemeType === "Another email templating solution") {
|
||||||
// NOTE: This is arbitrary
|
console.log(
|
||||||
startingFromMajor: 17,
|
[
|
||||||
excludeMajorVersions: [],
|
"There is currently no automated support for keycloakify-email, it has to be done manually, see documentation:",
|
||||||
doOmitPatch: false,
|
"https://docs.keycloakify.dev/theme-types/email-theme"
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPackageJson = (() => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
scripts?: Record<string, string | undefined>;
|
||||||
|
dependencies?: Record<string, string | undefined>;
|
||||||
|
devDependencies?: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
scripts: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||||
|
dependencies: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||||
|
devDependencies: z.record(z.union([z.string(), z.undefined()])).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson;
|
||||||
|
})();
|
||||||
|
|
||||||
|
addSyncExtensionsToPostinstallScript({
|
||||||
|
parsedPackageJson,
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
const moduleName = `@keycloakify/email-native`;
|
||||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
|
||||||
cacheDirPath: buildContext.cacheDirPath,
|
|
||||||
fetchOptions: buildContext.fetchOptions,
|
|
||||||
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
|
|
||||||
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
|
|
||||||
const fileRelativePath_target = pathRelative(
|
|
||||||
pathJoin("theme", "base", "email"),
|
|
||||||
fileRelativePath
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fileRelativePath_target.startsWith("..")) {
|
const [version] = ((): string[] => {
|
||||||
return;
|
const cmdOutput = child_process
|
||||||
}
|
.execSync(`npm show ${moduleName} versions --json`)
|
||||||
|
.toString("utf8")
|
||||||
|
.trim();
|
||||||
|
|
||||||
await writeFile({ fileRelativePath: fileRelativePath_target });
|
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||||
|
|
||||||
|
// NOTE: Bug in some older npm versions
|
||||||
|
if (typeof versions === "string") {
|
||||||
|
return [versions];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
transformCodebase({
|
return versions;
|
||||||
srcDirPath: extractedDirPath,
|
})()
|
||||||
destDirPath: emailThemeSrcDirPath
|
.reverse()
|
||||||
});
|
.filter(version => !version.includes("-"));
|
||||||
|
|
||||||
|
assert(version !== undefined);
|
||||||
|
|
||||||
|
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
|
||||||
|
|
||||||
|
if (parsedPackageJson.devDependencies !== undefined) {
|
||||||
|
delete parsedPackageJson.devDependencies[moduleName];
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
|
let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
|
||||||
|
|
||||||
|
if (await getIsPrettierAvailable()) {
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
sourceCode,
|
||||||
|
filePath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
themePropertyFilePath,
|
buildContext.packageJsonFilePath,
|
||||||
Buffer.from(
|
Buffer.from(sourceCode, "utf8")
|
||||||
[
|
|
||||||
`parent=base`,
|
|
||||||
fs.readFileSync(themePropertyFilePath).toString("utf8")
|
|
||||||
].join("\n"),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
await npmInstall({
|
||||||
`The \`${pathJoin(
|
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||||
".",
|
});
|
||||||
pathRelative(process.cwd(), emailThemeSrcDirPath)
|
|
||||||
)}\` directory have been created.`
|
console.log(chalk.green("Email theme initialized."));
|
||||||
);
|
|
||||||
console.log("You can delete any file you don't modify.");
|
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import { readFileSync } from "fs";
|
|||||||
import { isInside } from "../../tools/isInside";
|
import { isInside } from "../../tools/isInside";
|
||||||
import child_process from "child_process";
|
import child_process from "child_process";
|
||||||
import { rmSync } from "../../tools/fs.rmSync";
|
import { rmSync } from "../../tools/fs.rmSync";
|
||||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_generatePom & {
|
export type BuildContextLike = BuildContextLike_generatePom & {
|
||||||
keycloakifyBuildDirPath: string;
|
keycloakifyBuildDirPath: string;
|
||||||
@ -105,29 +105,55 @@ export async function buildJar(params: {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
remove_account_v1_in_meta_inf: {
|
{
|
||||||
if (!doesImplementAccountV1Theme) {
|
const filePath = pathJoin(
|
||||||
// NOTE: We do not have account v1 anyway
|
tmpResourcesDirPath,
|
||||||
break remove_account_v1_in_meta_inf;
|
"META-INF",
|
||||||
}
|
"keycloak-themes.json"
|
||||||
|
);
|
||||||
|
|
||||||
if (keycloakAccountV1Version !== null) {
|
await fs.mkdir(pathDirname(filePath));
|
||||||
// NOTE: No, we need to keep account-v1 in meta-inf
|
|
||||||
break remove_account_v1_in_meta_inf;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeMetaInfKeycloakThemes({
|
await fs.writeFile(
|
||||||
resourcesDirPath: tmpResourcesDirPath,
|
filePath,
|
||||||
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
|
Buffer.from(
|
||||||
assert(metaInfKeycloakTheme !== undefined);
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
themes: await (async () => {
|
||||||
|
const dirPath = pathJoin(tmpResourcesDirPath, "theme");
|
||||||
|
|
||||||
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
|
const themeNames = (await fs.readdir(dirPath)).sort(
|
||||||
({ name }) => name !== "account-v1"
|
(a, b) => {
|
||||||
);
|
const indexA = buildContext.themeNames.indexOf(a);
|
||||||
|
const indexB = buildContext.themeNames.indexOf(b);
|
||||||
|
|
||||||
return metaInfKeycloakTheme;
|
const orderA = indexA === -1 ? Infinity : indexA;
|
||||||
}
|
const orderB = indexB === -1 ? Infinity : indexB;
|
||||||
});
|
|
||||||
|
return orderA - orderB;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
themeNames.map(async themeName => {
|
||||||
|
const types = await fs.readdir(
|
||||||
|
pathJoin(dirPath, themeName)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: themeName,
|
||||||
|
types
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
route_legacy_pages: {
|
route_legacy_pages: {
|
||||||
@ -135,40 +161,49 @@ export async function buildJar(params: {
|
|||||||
break route_legacy_pages;
|
break route_legacy_pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
|
await Promise.all(
|
||||||
buildContext.themeNames.map(themeName => {
|
(["register.ftl", "login-update-profile.ftl"] as const)
|
||||||
const ftlFilePath = pathJoin(
|
.map(pageId =>
|
||||||
tmpResourcesDirPath,
|
buildContext.themeNames.map(async themeName => {
|
||||||
"theme",
|
const ftlFilePath = pathJoin(
|
||||||
themeName,
|
tmpResourcesDirPath,
|
||||||
"login",
|
"theme",
|
||||||
pageId
|
themeName,
|
||||||
);
|
"login",
|
||||||
|
pageId
|
||||||
|
);
|
||||||
|
|
||||||
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
|
// NOTE: https://github.com/keycloakify/keycloakify/issues/665
|
||||||
|
if (!(await existsAsync(ftlFilePath))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ftlFileBasename = (() => {
|
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
|
||||||
switch (pageId) {
|
|
||||||
case "register.ftl":
|
|
||||||
return "register-user-profile.ftl";
|
|
||||||
case "login-update-profile.ftl":
|
|
||||||
return "update-user-profile.ftl";
|
|
||||||
}
|
|
||||||
assert<Equals<typeof pageId, never>>(false);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const modifiedFtlFileContent = ftlFileContent.replace(
|
const ftlFileBasename = (() => {
|
||||||
`"ftlTemplateFileName": "${pageId}"`,
|
switch (pageId) {
|
||||||
`"ftlTemplateFileName": "${ftlFileBasename}"`
|
case "register.ftl":
|
||||||
);
|
return "register-user-profile.ftl";
|
||||||
|
case "login-update-profile.ftl":
|
||||||
|
return "update-user-profile.ftl";
|
||||||
|
}
|
||||||
|
assert<Equals<typeof pageId, never>>(false);
|
||||||
|
})();
|
||||||
|
|
||||||
assert(modifiedFtlFileContent !== ftlFileContent);
|
const modifiedFtlFileContent = ftlFileContent.replace(
|
||||||
|
`"ftlTemplateFileName": "${pageId}"`,
|
||||||
|
`"ftlTemplateFileName": "${ftlFileBasename}"`
|
||||||
|
);
|
||||||
|
|
||||||
fs.writeFile(
|
assert(modifiedFtlFileContent !== ftlFileContent);
|
||||||
pathJoin(pathDirname(ftlFilePath), ftlFileBasename),
|
|
||||||
Buffer.from(modifiedFtlFileContent, "utf8")
|
await fs.writeFile(
|
||||||
);
|
pathJoin(pathDirname(ftlFilePath), ftlFileBasename),
|
||||||
})
|
Buffer.from(modifiedFtlFileContent, "utf8")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,31 +220,39 @@ export async function buildJar(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) =>
|
{
|
||||||
child_process.exec(
|
const mvnBuildCmd = `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`;
|
||||||
`mvn install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
|
|
||||||
{ cwd: keycloakifyBuildCacheDirPath },
|
|
||||||
error => {
|
|
||||||
if (error !== null) {
|
|
||||||
console.error(
|
|
||||||
`Build jar failed: ${JSON.stringify(
|
|
||||||
{
|
|
||||||
jarFileBasename,
|
|
||||||
keycloakAccountV1Version,
|
|
||||||
keycloakThemeAdditionalInfoExtensionVersion
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
reject(error);
|
await new Promise<void>((resolve, reject) =>
|
||||||
return;
|
child_process.exec(
|
||||||
|
mvnBuildCmd,
|
||||||
|
{ cwd: keycloakifyBuildCacheDirPath },
|
||||||
|
error => {
|
||||||
|
if (error !== null) {
|
||||||
|
console.error(
|
||||||
|
[
|
||||||
|
`Build jar failed: ${JSON.stringify(
|
||||||
|
{
|
||||||
|
jarFileBasename,
|
||||||
|
keycloakAccountV1Version,
|
||||||
|
keycloakThemeAdditionalInfoExtensionVersion
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}`,
|
||||||
|
"Try running the following command to debug the issue (you are probably under a restricted network and you need to configure your proxy):",
|
||||||
|
`cd ${keycloakifyBuildCacheDirPath} && ${mvnBuildCmd}`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
resolve();
|
)
|
||||||
}
|
);
|
||||||
)
|
}
|
||||||
);
|
|
||||||
|
|
||||||
await fs.rename(
|
await fs.rename(
|
||||||
pathJoin(
|
pathJoin(
|
||||||
|
@ -52,9 +52,9 @@ export function getKeycloakVersionRangeForJar(params: {
|
|||||||
case "0.6":
|
case "0.6":
|
||||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||||
case null:
|
case null:
|
||||||
return undefined;
|
return "26-and-above" as const;
|
||||||
case "1.1.5":
|
case "1.1.5":
|
||||||
return "25-and-above" as const;
|
return "25" as const;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert<Equals<typeof keycloakAccountV1Version, never>>(false);
|
assert<Equals<typeof keycloakAccountV1Version, never>>(false);
|
||||||
@ -75,9 +75,9 @@ export function getKeycloakVersionRangeForJar(params: {
|
|||||||
}
|
}
|
||||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||||
case null:
|
case null:
|
||||||
return "21-and-below";
|
return "all-other-versions";
|
||||||
case "1.1.5":
|
case "1.1.5":
|
||||||
return "22-and-above";
|
return "22-to-25";
|
||||||
}
|
}
|
||||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(
|
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(
|
||||||
false
|
false
|
||||||
|
@ -11,7 +11,11 @@ import * as fs from "fs";
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import type { BuildContext } from "../../shared/buildContext";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { type ThemeType, WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants";
|
import {
|
||||||
|
type ThemeType,
|
||||||
|
WELL_KNOWN_DIRECTORY_BASE_NAME,
|
||||||
|
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
|
||||||
|
} from "../../shared/constants";
|
||||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
|
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
|
||||||
@ -116,6 +120,7 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
.replace("{{themeVersion}}", buildContext.themeVersion)
|
.replace("{{themeVersion}}", buildContext.themeVersion)
|
||||||
.replace("{{fieldNames}}", fieldNames.map(name => `"${name}"`).join(", "))
|
.replace("{{fieldNames}}", fieldNames.map(name => `"${name}"`).join(", "))
|
||||||
.replace("{{RESOURCES_COMMON}}", WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON)
|
.replace("{{RESOURCES_COMMON}}", WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON)
|
||||||
|
.replace("{{KEYCLOAKIFY_SPA_DEV_SERVER_PORT}}", KEYCLOAKIFY_SPA_DEV_SERVER_PORT)
|
||||||
.replace(
|
.replace(
|
||||||
"{{userDefinedExclusions}}",
|
"{{userDefinedExclusions}}",
|
||||||
buildContext.kcContextExclusionsFtlCode ?? ""
|
buildContext.kcContextExclusionsFtlCode ?? ""
|
||||||
|
@ -84,8 +84,47 @@ attributes_to_attributesByName: {
|
|||||||
kcContext.profile.attributesByName[attribute.name] = attribute;
|
kcContext.profile.attributesByName[attribute.name] = attribute;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redirect_to_dev_server: {
|
||||||
|
|
||||||
|
switch(kcContext.themeType){
|
||||||
|
case "login":
|
||||||
|
break redirect_to_dev_server;
|
||||||
|
case "account":
|
||||||
|
if( kcContext.pageId !== "index.ftl" ){
|
||||||
|
break redirect_to_dev_server;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "admin":
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break redirect_to_dev_server;
|
||||||
|
}
|
||||||
|
|
||||||
|
const devSeverPort = kcContext.properties.{{KEYCLOAKIFY_SPA_DEV_SERVER_PORT}};
|
||||||
|
|
||||||
|
if( !devSeverPort ){
|
||||||
|
break redirect_to_dev_server;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUrl = new URL(window.location.href);
|
||||||
|
|
||||||
|
redirectUrl.port = devSeverPort;
|
||||||
|
|
||||||
|
delete kcContext.msgJSON;
|
||||||
|
|
||||||
|
console.log(kcContext);
|
||||||
|
|
||||||
|
redirectUrl.searchParams.set("kcContext", encodeURIComponent(JSON.stringify(kcContext)));
|
||||||
|
|
||||||
|
window.location.href = redirectUrl.toString();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
window.kcContext = kcContext;
|
window.kcContext = kcContext;
|
||||||
|
|
||||||
|
|
||||||
<#if xKeycloakify.themeType == "login" >
|
<#if xKeycloakify.themeType == "login" >
|
||||||
{
|
{
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
@ -151,7 +190,7 @@ function decodeHtmlEntities(htmlStr){
|
|||||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
||||||
key == "loginAction" &&
|
key == "loginAction" &&
|
||||||
areSamePath(path, ["url"]) &&
|
areSamePath(path, ["url"]) &&
|
||||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl", "frontchannel-logout.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
||||||
!(auth?has_content && auth.showTryAnotherWayLink())
|
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||||
) || (
|
) || (
|
||||||
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
||||||
@ -166,7 +205,7 @@ function decodeHtmlEntities(htmlStr){
|
|||||||
areSamePath(path, []) &&
|
areSamePath(path, []) &&
|
||||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(xKeycloakify.pageId)
|
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(xKeycloakify.pageId)
|
||||||
) || (
|
) || (
|
||||||
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
["masterAdminClient", "delegateForUpdate", "defaultRole", "smtpConfig"]?seq_contains(key) &&
|
||||||
areSamePath(path, ["realm"])
|
areSamePath(path, ["realm"])
|
||||||
) || (
|
) || (
|
||||||
xKeycloakify.pageId == "error.ftl" &&
|
xKeycloakify.pageId == "error.ftl" &&
|
||||||
@ -235,6 +274,9 @@ function decodeHtmlEntities(htmlStr){
|
|||||||
"identityFederationEnabled",
|
"identityFederationEnabled",
|
||||||
"userManagedAccessAllowed"
|
"userManagedAccessAllowed"
|
||||||
]?seq_contains(key)
|
]?seq_contains(key)
|
||||||
|
) || (
|
||||||
|
["flowContext", "session", "realm"]?seq_contains(key) &&
|
||||||
|
areSamePath(path, ["social"])
|
||||||
)
|
)
|
||||||
>
|
>
|
||||||
<#-- <#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
|
<#-- <#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
|
||||||
|
@ -22,7 +22,7 @@ assert<BuildContext extends BuildContextLike ? true : false>();
|
|||||||
|
|
||||||
export function generateMessageProperties(params: {
|
export function generateMessageProperties(params: {
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
themeType: ThemeType;
|
themeType: Exclude<ThemeType, "admin">;
|
||||||
}): {
|
}): {
|
||||||
languageTags: string[];
|
languageTags: string[];
|
||||||
writeMessagePropertiesFiles: (params: {
|
writeMessagePropertiesFiles: (params: {
|
||||||
|
@ -1,16 +1,55 @@
|
|||||||
import type { BuildContext } from "../../shared/buildContext";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import {
|
|
||||||
generateResourcesForMainTheme,
|
|
||||||
type BuildContextLike as BuildContextLike_generateResourcesForMainTheme
|
|
||||||
} from "./generateResourcesForMainTheme";
|
|
||||||
import { generateResourcesForThemeVariant } from "./generateResourcesForThemeVariant";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { rmSync } from "../../tools/fs.rmSync";
|
import { rmSync } from "../../tools/fs.rmSync";
|
||||||
|
import { transformCodebase } from "../../tools/transformCodebase";
|
||||||
|
import {
|
||||||
|
join as pathJoin,
|
||||||
|
relative as pathRelative,
|
||||||
|
dirname as pathDirname,
|
||||||
|
basename as pathBasename
|
||||||
|
} from "path";
|
||||||
|
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||||
|
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||||
|
import {
|
||||||
|
generateFtlFilesCodeFactory,
|
||||||
|
type BuildContextLike as BuildContextLike_kcContextExclusionsFtlCode
|
||||||
|
} from "../generateFtl";
|
||||||
|
import {
|
||||||
|
type ThemeType,
|
||||||
|
LOGIN_THEME_PAGE_IDS,
|
||||||
|
ACCOUNT_THEME_PAGE_IDS,
|
||||||
|
WELL_KNOWN_DIRECTORY_BASE_NAME,
|
||||||
|
THEME_TYPES,
|
||||||
|
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
|
||||||
|
} from "../../shared/constants";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||||
|
import { readExtraPagesNames } from "./readExtraPageNames";
|
||||||
|
import {
|
||||||
|
generateMessageProperties,
|
||||||
|
type BuildContextLike as BuildContextLike_generateMessageProperties
|
||||||
|
} from "./generateMessageProperties";
|
||||||
|
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||||
|
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||||
|
import propertiesParser from "properties-parser";
|
||||||
|
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
||||||
|
import { listInstalledModules } from "../../tools/listInstalledModules";
|
||||||
|
import { isInside } from "../../tools/isInside";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_generateResourcesForMainTheme & {
|
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||||
themeNames: string[];
|
BuildContextLike_generateMessageProperties & {
|
||||||
};
|
themeNames: string[];
|
||||||
|
extraThemeProperties: string[] | undefined;
|
||||||
|
projectDirPath: string;
|
||||||
|
projectBuildDirPath: string;
|
||||||
|
environmentVariables: { name: string; default: string }[];
|
||||||
|
implementedThemeTypes: BuildContext["implementedThemeTypes"];
|
||||||
|
themeSrcDirPath: string;
|
||||||
|
bundler: "vite" | "webpack";
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
@ -18,27 +57,700 @@ export async function generateResources(params: {
|
|||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
resourcesDirPath: string;
|
resourcesDirPath: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
const { resourcesDirPath, buildContext } = params;
|
const { resourcesDirPath, buildContext } = params;
|
||||||
|
|
||||||
const [themeName, ...themeVariantNames] = buildContext.themeNames;
|
const [themeName] = buildContext.themeNames;
|
||||||
|
|
||||||
if (fs.existsSync(resourcesDirPath)) {
|
if (fs.existsSync(resourcesDirPath)) {
|
||||||
rmSync(resourcesDirPath, { recursive: true });
|
rmSync(resourcesDirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { writeMessagePropertiesFilesForThemeVariant } =
|
const getThemeTypeDirPath = (params: {
|
||||||
await generateResourcesForMainTheme({
|
themeType: ThemeType | "email";
|
||||||
resourcesDirPath,
|
themeName: string;
|
||||||
themeName,
|
}) => {
|
||||||
buildContext
|
const { themeType, themeName } = params;
|
||||||
});
|
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
|
||||||
|
};
|
||||||
|
|
||||||
for (const themeVariantName of themeVariantNames) {
|
const writeMessagePropertiesFilesByThemeType: Partial<
|
||||||
generateResourcesForThemeVariant({
|
Record<
|
||||||
resourcesDirPath,
|
ThemeType | "email",
|
||||||
themeName,
|
(params: { messageDirPath: string; themeName: string }) => void
|
||||||
themeVariantName,
|
>
|
||||||
writeMessagePropertiesFiles: writeMessagePropertiesFilesForThemeVariant
|
> = {};
|
||||||
});
|
|
||||||
|
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||||
|
let isNative: boolean;
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = buildContext.implementedThemeTypes[themeType];
|
||||||
|
|
||||||
|
if (!v.isImplemented && !v.isImplemented_native) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNative = !v.isImplemented && v.isImplemented_native;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAccountThemeType = () => {
|
||||||
|
assert(themeType === "account");
|
||||||
|
|
||||||
|
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||||
|
|
||||||
|
return buildContext.implementedThemeTypes.account.type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSpa = (() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return false;
|
||||||
|
case "account":
|
||||||
|
return getAccountThemeType() === "Single-Page";
|
||||||
|
case "admin":
|
||||||
|
return true;
|
||||||
|
case "email":
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
||||||
|
|
||||||
|
apply_replacers_and_move_to_theme_resources: {
|
||||||
|
if (isNative) {
|
||||||
|
break apply_replacers_and_move_to_theme_resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
const destDirPath = pathJoin(
|
||||||
|
themeTypeDirPath,
|
||||||
|
"resources",
|
||||||
|
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
|
||||||
|
);
|
||||||
|
|
||||||
|
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
|
||||||
|
rmSync(destDirPath, { recursive: true, force: true });
|
||||||
|
|
||||||
|
if (
|
||||||
|
themeType !== "login" &&
|
||||||
|
buildContext.implementedThemeTypes.login.isImplemented
|
||||||
|
) {
|
||||||
|
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: pathJoin(
|
||||||
|
getThemeTypeDirPath({
|
||||||
|
themeName,
|
||||||
|
themeType: "login"
|
||||||
|
}),
|
||||||
|
"resources",
|
||||||
|
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
|
||||||
|
),
|
||||||
|
destDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
break apply_replacers_and_move_to_theme_resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const dirPath = pathJoin(
|
||||||
|
buildContext.projectBuildDirPath,
|
||||||
|
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(dirPath)) {
|
||||||
|
assert(buildContext.bundler === "webpack");
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
`Keycloakify build error: The ${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES} directory shouldn't exist in your build directory.`,
|
||||||
|
`(${pathRelative(process.cwd(), dirPath)}).\n`,
|
||||||
|
`Theses assets are only required for local development with Storybook.",
|
||||||
|
"Please remove this directory as an additional step of your command.\n`,
|
||||||
|
`For example: \`"build": "... && rimraf ${pathRelative(buildContext.projectDirPath, dirPath)}"\``
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: buildContext.projectBuildDirPath,
|
||||||
|
destDirPath,
|
||||||
|
transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => {
|
||||||
|
if (filePath.endsWith(".css")) {
|
||||||
|
const { fixedCssCode } = replaceImportsInCssCode({
|
||||||
|
cssCode: sourceCode.toString("utf8"),
|
||||||
|
cssFileRelativeDirPath: pathDirname(fileRelativePath),
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath.endsWith(".js")) {
|
||||||
|
const { fixedJsCode } = replaceImportsInJsCode({
|
||||||
|
jsCode: sourceCode.toString("utf8"),
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedSourceCode: Buffer.from(fixedJsCode, "utf8")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode: sourceCode };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_ftl_files: {
|
||||||
|
if (isNative) {
|
||||||
|
break generate_ftl_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(themeType !== "email");
|
||||||
|
|
||||||
|
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||||
|
themeName,
|
||||||
|
indexHtmlCode: fs
|
||||||
|
.readFileSync(
|
||||||
|
pathJoin(buildContext.projectBuildDirPath, "index.html")
|
||||||
|
)
|
||||||
|
.toString("utf8"),
|
||||||
|
buildContext,
|
||||||
|
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||||
|
themeType,
|
||||||
|
fieldNames: isSpa
|
||||||
|
? []
|
||||||
|
: (assert(themeType !== "admin"),
|
||||||
|
readFieldNameUsage({
|
||||||
|
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||||
|
themeType
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
...(() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return LOGIN_THEME_PAGE_IDS;
|
||||||
|
case "account":
|
||||||
|
return getAccountThemeType() === "Single-Page"
|
||||||
|
? ["index.ftl"]
|
||||||
|
: ACCOUNT_THEME_PAGE_IDS;
|
||||||
|
case "admin":
|
||||||
|
return ["index.ftl"];
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
...(isSpa
|
||||||
|
? []
|
||||||
|
: readExtraPagesNames({
|
||||||
|
themeType,
|
||||||
|
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||||
|
}))
|
||||||
|
].forEach(pageId => {
|
||||||
|
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(themeTypeDirPath, pageId),
|
||||||
|
Buffer.from(ftlCode, "utf8")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_native_theme: {
|
||||||
|
if (!isNative) {
|
||||||
|
break copy_native_theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: dirPath,
|
||||||
|
destDirPath: getThemeTypeDirPath({ themeName, themeType }),
|
||||||
|
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||||
|
if (isInside({ dirPath: "messages", filePath: fileRelativePath })) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode: sourceCode };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let languageTags: string[] | undefined = undefined;
|
||||||
|
|
||||||
|
i18n_multi_page: {
|
||||||
|
if (isNative) {
|
||||||
|
break i18n_multi_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSpa) {
|
||||||
|
break i18n_multi_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(themeType !== "admin" && themeType !== "email");
|
||||||
|
|
||||||
|
const wrap = generateMessageProperties({
|
||||||
|
buildContext,
|
||||||
|
themeType
|
||||||
|
});
|
||||||
|
|
||||||
|
languageTags = wrap.languageTags;
|
||||||
|
const { writeMessagePropertiesFiles } = wrap;
|
||||||
|
|
||||||
|
writeMessagePropertiesFilesByThemeType[themeType] =
|
||||||
|
writeMessagePropertiesFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isLegacyAccountSpa = false;
|
||||||
|
|
||||||
|
// NOTE: Eventually remove this block.
|
||||||
|
i18n_single_page_account_legacy: {
|
||||||
|
if (!isSpa) {
|
||||||
|
break i18n_single_page_account_legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeType !== "account") {
|
||||||
|
break i18n_single_page_account_legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [moduleMeta] = await listInstalledModules({
|
||||||
|
packageJsonFilePath: buildContext.packageJsonFilePath,
|
||||||
|
filter: ({ moduleName }) =>
|
||||||
|
moduleName === "@keycloakify/keycloak-account-ui"
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(
|
||||||
|
moduleMeta !== undefined,
|
||||||
|
`@keycloakify/keycloak-account-ui is supposed to be installed`
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const [majorStr] = moduleMeta.version.split(".");
|
||||||
|
|
||||||
|
if (majorStr.length === 6) {
|
||||||
|
// NOTE: Now we use the format MMmmpp (Major, minor, patch) for example for
|
||||||
|
// 26.0.7 it would be 260007.
|
||||||
|
break i18n_single_page_account_legacy;
|
||||||
|
} else {
|
||||||
|
// 25.0.4-rc.5 or later
|
||||||
|
isLegacyAccountSpa = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageDirPath_defaults = pathJoin(moduleMeta.dirPath, "messages");
|
||||||
|
|
||||||
|
if (!fs.existsSync(messageDirPath_defaults)) {
|
||||||
|
throw new Error(
|
||||||
|
`Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLegacyAccountSpa = true;
|
||||||
|
|
||||||
|
const messagesDirPath_dest = pathJoin(
|
||||||
|
getThemeTypeDirPath({ themeName, themeType: "account" }),
|
||||||
|
"messages"
|
||||||
|
);
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: messageDirPath_defaults,
|
||||||
|
destDirPath: messagesDirPath_dest
|
||||||
|
});
|
||||||
|
|
||||||
|
apply_theme_changes: {
|
||||||
|
const messagesDirPath_theme = pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"account",
|
||||||
|
"messages"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(messagesDirPath_theme)) {
|
||||||
|
break apply_theme_changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
|
||||||
|
const filePath_src = pathJoin(messagesDirPath_theme, basename);
|
||||||
|
const filePath_dest = pathJoin(messagesDirPath_dest, basename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath_dest)) {
|
||||||
|
fs.cpSync(filePath_src, filePath_dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages_src = propertiesParser.parse(
|
||||||
|
fs.readFileSync(filePath_src).toString("utf8")
|
||||||
|
);
|
||||||
|
const messages_dest = propertiesParser.parse(
|
||||||
|
fs.readFileSync(filePath_dest).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
...messages_dest,
|
||||||
|
...messages_src
|
||||||
|
};
|
||||||
|
|
||||||
|
const editor = propertiesParser.createEditor();
|
||||||
|
|
||||||
|
Object.entries(messages).forEach(([key, value]) => {
|
||||||
|
editor.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
filePath_dest,
|
||||||
|
Buffer.from(editor.toString(), "utf8")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
languageTags = fs
|
||||||
|
.readdirSync(messagesDirPath_dest)
|
||||||
|
.map(basename =>
|
||||||
|
basename.replace(/^messages_/, "").replace(/\.properties$/, "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
i18n_for_spas_and_native: {
|
||||||
|
if (!isSpa && !isNative) {
|
||||||
|
break i18n_for_spas_and_native;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLegacyAccountSpa) {
|
||||||
|
break i18n_for_spas_and_native;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesDirPath_theme = pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
themeType,
|
||||||
|
isNative ? "messages" : "i18n"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(messagesDirPath_theme)) {
|
||||||
|
break i18n_for_spas_and_native;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertiesByLang: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
base: Buffer;
|
||||||
|
override: Buffer | undefined;
|
||||||
|
overrideByThemeName: Record<string, Buffer>;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
|
||||||
|
type ParsedBasename = { lang: string } & (
|
||||||
|
| {
|
||||||
|
isOverride: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isOverride: true;
|
||||||
|
themeName: string | undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedBasename = ((): ParsedBasename | undefined => {
|
||||||
|
const match = basename.match(/^messages_([^.]+)\.properties$/);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discriminator = match[1];
|
||||||
|
|
||||||
|
const split = discriminator.split("_override");
|
||||||
|
|
||||||
|
if (split.length === 1) {
|
||||||
|
return {
|
||||||
|
lang: discriminator,
|
||||||
|
isOverride: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(split.length === 2);
|
||||||
|
|
||||||
|
if (split[1] === "") {
|
||||||
|
return {
|
||||||
|
lang: split[0],
|
||||||
|
isOverride: true,
|
||||||
|
themeName: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const match2 = split[1].match(/^_(.+)$/);
|
||||||
|
|
||||||
|
assert(match2 !== null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
lang: split[0],
|
||||||
|
isOverride: true,
|
||||||
|
themeName: match2[1]
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (parsedBasename === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertiesByLang[parsedBasename.lang] ??= {
|
||||||
|
base: createObjectThatThrowsIfAccessed<Buffer>({
|
||||||
|
debugMessage: `No base ${parsedBasename.lang} translation for ${themeType} theme`
|
||||||
|
}),
|
||||||
|
override: undefined,
|
||||||
|
overrideByThemeName: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buffer = fs.readFileSync(pathJoin(messagesDirPath_theme, basename));
|
||||||
|
|
||||||
|
if (parsedBasename.isOverride === false) {
|
||||||
|
propertiesByLang[parsedBasename.lang].base = buffer;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedBasename.themeName === undefined) {
|
||||||
|
propertiesByLang[parsedBasename.lang].override = buffer;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertiesByLang[parsedBasename.lang].overrideByThemeName[
|
||||||
|
parsedBasename.themeName
|
||||||
|
] = buffer;
|
||||||
|
});
|
||||||
|
|
||||||
|
languageTags = Object.keys(propertiesByLang);
|
||||||
|
|
||||||
|
writeMessagePropertiesFilesByThemeType[themeType] = ({
|
||||||
|
messageDirPath,
|
||||||
|
themeName
|
||||||
|
}) => {
|
||||||
|
if (!fs.existsSync(messageDirPath)) {
|
||||||
|
fs.mkdirSync(messageDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(propertiesByLang).forEach(
|
||||||
|
([lang, { base, override, overrideByThemeName }]) => {
|
||||||
|
const messages = propertiesParser.parse(base.toString("utf8"));
|
||||||
|
|
||||||
|
if (override !== undefined) {
|
||||||
|
const overrideMessages = propertiesParser.parse(
|
||||||
|
override.toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.entries(overrideMessages).forEach(
|
||||||
|
([key, value]) => (messages[key] = value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeName in overrideByThemeName) {
|
||||||
|
const overrideMessages = propertiesParser.parse(
|
||||||
|
overrideByThemeName[themeName].toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.entries(overrideMessages).forEach(
|
||||||
|
([key, value]) => (messages[key] = value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = propertiesParser.createEditor();
|
||||||
|
|
||||||
|
Object.entries(messages).forEach(([key, value]) => {
|
||||||
|
editor.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(messageDirPath, `messages_${lang}.properties`),
|
||||||
|
Buffer.from(editor.toString(), "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
keycloak_static_resources: {
|
||||||
|
if (isNative) {
|
||||||
|
break keycloak_static_resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSpa) {
|
||||||
|
break keycloak_static_resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"res",
|
||||||
|
"public",
|
||||||
|
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES,
|
||||||
|
themeType
|
||||||
|
),
|
||||||
|
destDirPath: pathJoin(themeTypeDirPath, "resources")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bring_in_account_v1: {
|
||||||
|
if (isNative) {
|
||||||
|
break bring_in_account_v1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeType !== "account") {
|
||||||
|
break bring_in_account_v1;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||||
|
|
||||||
|
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
||||||
|
break bring_in_account_v1;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
||||||
|
destDirPath: getThemeTypeDirPath({
|
||||||
|
themeName: "account-v1",
|
||||||
|
themeType: "account"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_theme_properties: {
|
||||||
|
if (isNative) {
|
||||||
|
break generate_theme_properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(themeType !== "email");
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(themeTypeDirPath, "theme.properties"),
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
`parent=${(() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "account":
|
||||||
|
switch (getAccountThemeType()) {
|
||||||
|
case "Multi-Page":
|
||||||
|
return "account-v1";
|
||||||
|
case "Single-Page":
|
||||||
|
return "base";
|
||||||
|
}
|
||||||
|
case "login":
|
||||||
|
return "keycloak";
|
||||||
|
case "admin":
|
||||||
|
return "base";
|
||||||
|
}
|
||||||
|
assert<Equals<typeof themeType, never>>;
|
||||||
|
})()}`,
|
||||||
|
...(themeType === "account" &&
|
||||||
|
getAccountThemeType() === "Single-Page"
|
||||||
|
? ["deprecatedMode=false"]
|
||||||
|
: []),
|
||||||
|
...(buildContext.extraThemeProperties ?? []),
|
||||||
|
...[
|
||||||
|
...buildContext.environmentVariables,
|
||||||
|
{ name: KEYCLOAKIFY_SPA_DEV_SERVER_PORT, default: "" }
|
||||||
|
].map(
|
||||||
|
({ name, default: defaultValue }) =>
|
||||||
|
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
||||||
|
),
|
||||||
|
...(languageTags === undefined
|
||||||
|
? []
|
||||||
|
: [`locales=${languageTags.join(",")}`])
|
||||||
|
].join("\n\n"),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const themeVariantName of [...buildContext.themeNames].reverse()) {
|
||||||
|
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||||
|
copy_main_theme_to_theme_variant_theme: {
|
||||||
|
let isNative: boolean;
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = buildContext.implementedThemeTypes[themeType];
|
||||||
|
|
||||||
|
if (!v.isImplemented && !v.isImplemented_native) {
|
||||||
|
break copy_main_theme_to_theme_variant_theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNative = !v.isImplemented && v.isImplemented_native;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNative && themeVariantName === themeName) {
|
||||||
|
break copy_main_theme_to_theme_variant_theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: getThemeTypeDirPath({ themeName, themeType }),
|
||||||
|
destDirPath: getThemeTypeDirPath({
|
||||||
|
themeName: themeVariantName,
|
||||||
|
themeType
|
||||||
|
}),
|
||||||
|
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||||
|
patch_xKeycloakify_themeName: {
|
||||||
|
if (!fileRelativePath.endsWith(".ftl")) {
|
||||||
|
break patch_xKeycloakify_themeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isNative &&
|
||||||
|
pathBasename(fileRelativePath) !== fileRelativePath
|
||||||
|
) {
|
||||||
|
break patch_xKeycloakify_themeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedSourceCode = Buffer.from(
|
||||||
|
Buffer.from(sourceCode)
|
||||||
|
.toString("utf-8")
|
||||||
|
.replace(
|
||||||
|
...id<[string | RegExp, string]>(
|
||||||
|
isNative
|
||||||
|
? [
|
||||||
|
/xKeycloakify\.themeName/g,
|
||||||
|
`"${themeVariantName}"`
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`"themeName": "${themeName}"`,
|
||||||
|
`"themeName": "${themeVariantName}"`
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isNative) {
|
||||||
|
assert(
|
||||||
|
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode: sourceCode };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
run_writeMessagePropertiesFiles: {
|
||||||
|
const writeMessagePropertiesFiles =
|
||||||
|
writeMessagePropertiesFilesByThemeType[themeType];
|
||||||
|
|
||||||
|
if (writeMessagePropertiesFiles === undefined) {
|
||||||
|
break run_writeMessagePropertiesFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeMessagePropertiesFiles({
|
||||||
|
messageDirPath: pathJoin(
|
||||||
|
getThemeTypeDirPath({ themeName: themeVariantName, themeType }),
|
||||||
|
"messages"
|
||||||
|
),
|
||||||
|
themeName: themeVariantName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Generated resources in ${Date.now() - start}ms`);
|
||||||
}
|
}
|
||||||
|
@ -1,373 +0,0 @@
|
|||||||
import { transformCodebase } from "../../tools/transformCodebase";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
|
||||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
|
||||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
|
||||||
import {
|
|
||||||
generateFtlFilesCodeFactory,
|
|
||||||
type BuildContextLike as BuildContextLike_kcContextExclusionsFtlCode
|
|
||||||
} from "../generateFtl";
|
|
||||||
import {
|
|
||||||
type ThemeType,
|
|
||||||
LOGIN_THEME_PAGE_IDS,
|
|
||||||
ACCOUNT_THEME_PAGE_IDS,
|
|
||||||
WELL_KNOWN_DIRECTORY_BASE_NAME
|
|
||||||
} from "../../shared/constants";
|
|
||||||
import type { BuildContext } from "../../shared/buildContext";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
|
||||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
|
||||||
import { readExtraPagesNames } from "./readExtraPageNames";
|
|
||||||
import {
|
|
||||||
generateMessageProperties,
|
|
||||||
type BuildContextLike as BuildContextLike_generateMessageProperties
|
|
||||||
} from "./generateMessageProperties";
|
|
||||||
import { rmSync } from "../../tools/fs.rmSync";
|
|
||||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
|
||||||
import {
|
|
||||||
writeMetaInfKeycloakThemes,
|
|
||||||
type MetaInfKeycloakTheme
|
|
||||||
} from "../../shared/metaInfKeycloakThemes";
|
|
||||||
import { objectEntries } from "tsafe/objectEntries";
|
|
||||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
|
||||||
import * as child_process from "child_process";
|
|
||||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
|
||||||
BuildContextLike_generateMessageProperties & {
|
|
||||||
extraThemeProperties: string[] | undefined;
|
|
||||||
projectDirPath: string;
|
|
||||||
projectBuildDirPath: string;
|
|
||||||
environmentVariables: { name: string; default: string }[];
|
|
||||||
implementedThemeTypes: BuildContext["implementedThemeTypes"];
|
|
||||||
themeSrcDirPath: string;
|
|
||||||
bundler: "vite" | "webpack";
|
|
||||||
packageJsonFilePath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export async function generateResourcesForMainTheme(params: {
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
themeName: string;
|
|
||||||
resourcesDirPath: string;
|
|
||||||
}): Promise<{
|
|
||||||
writeMessagePropertiesFilesForThemeVariant: (params: {
|
|
||||||
getMessageDirPath: (params: { themeType: ThemeType }) => string;
|
|
||||||
themeName: string;
|
|
||||||
}) => void;
|
|
||||||
}> {
|
|
||||||
const { themeName, resourcesDirPath, buildContext } = params;
|
|
||||||
|
|
||||||
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
|
|
||||||
const { themeType } = params;
|
|
||||||
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeMessagePropertiesFilesByThemeType: Partial<
|
|
||||||
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
for (const themeType of ["login", "account"] as const) {
|
|
||||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isForAccountSpa =
|
|
||||||
themeType === "account" &&
|
|
||||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
|
||||||
buildContext.implementedThemeTypes.account.type === "Single-Page");
|
|
||||||
|
|
||||||
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
|
|
||||||
|
|
||||||
apply_replacers_and_move_to_theme_resources: {
|
|
||||||
const destDirPath = pathJoin(
|
|
||||||
themeTypeDirPath,
|
|
||||||
"resources",
|
|
||||||
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
|
|
||||||
);
|
|
||||||
|
|
||||||
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
|
|
||||||
rmSync(destDirPath, { recursive: true, force: true });
|
|
||||||
|
|
||||||
if (
|
|
||||||
themeType === "account" &&
|
|
||||||
buildContext.implementedThemeTypes.login.isImplemented
|
|
||||||
) {
|
|
||||||
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: pathJoin(
|
|
||||||
getThemeTypeDirPath({
|
|
||||||
themeType: "login"
|
|
||||||
}),
|
|
||||||
"resources",
|
|
||||||
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
|
|
||||||
),
|
|
||||||
destDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
break apply_replacers_and_move_to_theme_resources;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const dirPath = pathJoin(
|
|
||||||
buildContext.projectBuildDirPath,
|
|
||||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fs.existsSync(dirPath)) {
|
|
||||||
assert(buildContext.bundler === "webpack");
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
[
|
|
||||||
`Keycloakify build error: The ${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES} directory shouldn't exist in your build directory.`,
|
|
||||||
`(${pathRelative(process.cwd(), dirPath)}).\n`,
|
|
||||||
`Theses assets are only required for local development with Storybook.",
|
|
||||||
"Please remove this directory as an additional step of your command.\n`,
|
|
||||||
`For example: \`"build": "... && rimraf ${pathRelative(buildContext.projectDirPath, dirPath)}"\``
|
|
||||||
].join(" ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: buildContext.projectBuildDirPath,
|
|
||||||
destDirPath,
|
|
||||||
transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => {
|
|
||||||
if (filePath.endsWith(".css")) {
|
|
||||||
const { fixedCssCode } = replaceImportsInCssCode({
|
|
||||||
cssCode: sourceCode.toString("utf8"),
|
|
||||||
cssFileRelativeDirPath: pathDirname(fileRelativePath),
|
|
||||||
buildContext
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filePath.endsWith(".js")) {
|
|
||||||
const { fixedJsCode } = replaceImportsInJsCode({
|
|
||||||
jsCode: sourceCode.toString("utf8"),
|
|
||||||
buildContext
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
modifiedSourceCode: Buffer.from(fixedJsCode, "utf8")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { modifiedSourceCode: sourceCode };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
|
||||||
themeName,
|
|
||||||
indexHtmlCode: fs
|
|
||||||
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
|
|
||||||
.toString("utf8"),
|
|
||||||
buildContext,
|
|
||||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
|
||||||
themeType,
|
|
||||||
fieldNames: readFieldNameUsage({
|
|
||||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
|
||||||
themeType
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
[
|
|
||||||
...(() => {
|
|
||||||
switch (themeType) {
|
|
||||||
case "login":
|
|
||||||
return LOGIN_THEME_PAGE_IDS;
|
|
||||||
case "account":
|
|
||||||
return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
...(isForAccountSpa
|
|
||||||
? []
|
|
||||||
: readExtraPagesNames({
|
|
||||||
themeType,
|
|
||||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
|
||||||
}))
|
|
||||||
].forEach(pageId => {
|
|
||||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(themeTypeDirPath, pageId),
|
|
||||||
Buffer.from(ftlCode, "utf8")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let languageTags: string[] | undefined = undefined;
|
|
||||||
|
|
||||||
i18n_messages_generation: {
|
|
||||||
if (isForAccountSpa) {
|
|
||||||
break i18n_messages_generation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrap = generateMessageProperties({
|
|
||||||
buildContext,
|
|
||||||
themeType
|
|
||||||
});
|
|
||||||
|
|
||||||
languageTags = wrap.languageTags;
|
|
||||||
const { writeMessagePropertiesFiles } = wrap;
|
|
||||||
|
|
||||||
writeMessagePropertiesFilesByThemeType[themeType] =
|
|
||||||
writeMessagePropertiesFiles;
|
|
||||||
|
|
||||||
writeMessagePropertiesFiles({
|
|
||||||
messageDirPath: pathJoin(themeTypeDirPath, "messages"),
|
|
||||||
themeName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bring_in_account_v3_i18n_messages: {
|
|
||||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
|
||||||
break bring_in_account_v3_i18n_messages;
|
|
||||||
}
|
|
||||||
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
|
|
||||||
break bring_in_account_v3_i18n_messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountUiDirPath = child_process
|
|
||||||
.execSync("npm list @keycloakify/keycloak-account-ui --parseable", {
|
|
||||||
cwd: pathDirname(buildContext.packageJsonFilePath)
|
|
||||||
})
|
|
||||||
.toString("utf8")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
const messagesDirPath = pathJoin(accountUiDirPath, "messages");
|
|
||||||
|
|
||||||
if (!fs.existsSync(messagesDirPath)) {
|
|
||||||
throw new Error(
|
|
||||||
`Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: messagesDirPath,
|
|
||||||
destDirPath: pathJoin(
|
|
||||||
getThemeTypeDirPath({ themeType: "account" }),
|
|
||||||
"messages"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
keycloak_static_resources: {
|
|
||||||
if (isForAccountSpa) {
|
|
||||||
break keycloak_static_resources;
|
|
||||||
}
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: pathJoin(
|
|
||||||
getThisCodebaseRootDirPath(),
|
|
||||||
"res",
|
|
||||||
"public",
|
|
||||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES,
|
|
||||||
themeType
|
|
||||||
),
|
|
||||||
destDirPath: pathJoin(themeTypeDirPath, "resources")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(themeTypeDirPath, "theme.properties"),
|
|
||||||
Buffer.from(
|
|
||||||
[
|
|
||||||
`parent=${(() => {
|
|
||||||
switch (themeType) {
|
|
||||||
case "account":
|
|
||||||
return isForAccountSpa ? "base" : "account-v1";
|
|
||||||
case "login":
|
|
||||||
return "keycloak";
|
|
||||||
}
|
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
|
||||||
})()}`,
|
|
||||||
...(isForAccountSpa ? ["deprecatedMode=false"] : []),
|
|
||||||
...(buildContext.extraThemeProperties ?? []),
|
|
||||||
...buildContext.environmentVariables.map(
|
|
||||||
({ name, default: defaultValue }) =>
|
|
||||||
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
|
||||||
),
|
|
||||||
...(languageTags === undefined
|
|
||||||
? []
|
|
||||||
: [`locales=${languageTags.join(",")}`])
|
|
||||||
].join("\n\n"),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
email: {
|
|
||||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
|
||||||
break email;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: emailThemeSrcDirPath,
|
|
||||||
destDirPath: getThemeTypeDirPath({ themeType: "email" })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bring_in_account_v1: {
|
|
||||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
|
||||||
break bring_in_account_v1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
|
||||||
break bring_in_account_v1;
|
|
||||||
}
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
|
||||||
destDirPath: pathJoin(resourcesDirPath, "theme", "account-v1", "account")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
|
|
||||||
|
|
||||||
metaInfKeycloakThemes.themes.push({
|
|
||||||
name: themeName,
|
|
||||||
types: objectEntries(buildContext.implementedThemeTypes)
|
|
||||||
.filter(([, { isImplemented }]) => isImplemented)
|
|
||||||
.map(([themeType]) => themeType)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (buildContext.implementedThemeTypes.account.isImplemented) {
|
|
||||||
metaInfKeycloakThemes.themes.push({
|
|
||||||
name: "account-v1",
|
|
||||||
types: ["account"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
writeMetaInfKeycloakThemes({
|
|
||||||
resourcesDirPath,
|
|
||||||
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
writeMessagePropertiesFilesForThemeVariant: ({
|
|
||||||
getMessageDirPath,
|
|
||||||
themeName
|
|
||||||
}) => {
|
|
||||||
objectEntries(writeMessagePropertiesFilesByThemeType).forEach(
|
|
||||||
([themeType, writeMessagePropertiesFiles]) => {
|
|
||||||
if (writeMessagePropertiesFiles === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
writeMessagePropertiesFiles({
|
|
||||||
messageDirPath: getMessageDirPath({ themeType }),
|
|
||||||
themeName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
|
|
||||||
import { transformCodebase } from "../../tools/transformCodebase";
|
|
||||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import type { ThemeType } from "../../shared/constants";
|
|
||||||
|
|
||||||
export function generateResourcesForThemeVariant(params: {
|
|
||||||
resourcesDirPath: string;
|
|
||||||
themeName: string;
|
|
||||||
themeVariantName: string;
|
|
||||||
writeMessagePropertiesFiles: (params: {
|
|
||||||
getMessageDirPath: (params: { themeType: ThemeType }) => string;
|
|
||||||
themeName: string;
|
|
||||||
}) => void;
|
|
||||||
}) {
|
|
||||||
const { resourcesDirPath, themeName, themeVariantName, writeMessagePropertiesFiles } =
|
|
||||||
params;
|
|
||||||
|
|
||||||
const mainThemeDirPath = pathJoin(resourcesDirPath, "theme", themeName);
|
|
||||||
const themeVariantDirPath = pathJoin(mainThemeDirPath, "..", themeVariantName);
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: mainThemeDirPath,
|
|
||||||
destDirPath: themeVariantDirPath,
|
|
||||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
|
||||||
if (
|
|
||||||
pathExtname(fileRelativePath) === ".ftl" &&
|
|
||||||
fileRelativePath.split(pathSep).length === 2
|
|
||||||
) {
|
|
||||||
const modifiedSourceCode = Buffer.from(
|
|
||||||
Buffer.from(sourceCode)
|
|
||||||
.toString("utf-8")
|
|
||||||
.replace(
|
|
||||||
`"themeName": "${themeName}"`,
|
|
||||||
`"themeName": "${themeVariantName}"`
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
|
|
||||||
|
|
||||||
return { modifiedSourceCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { modifiedSourceCode: sourceCode };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
writeMetaInfKeycloakThemes({
|
|
||||||
resourcesDirPath,
|
|
||||||
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
|
|
||||||
assert(metaInfKeycloakTheme !== undefined);
|
|
||||||
|
|
||||||
const newMetaInfKeycloakTheme = metaInfKeycloakTheme;
|
|
||||||
|
|
||||||
newMetaInfKeycloakTheme.themes.push({
|
|
||||||
name: themeVariantName,
|
|
||||||
types: (() => {
|
|
||||||
const theme = newMetaInfKeycloakTheme.themes.find(
|
|
||||||
({ name }) => name === themeName
|
|
||||||
);
|
|
||||||
assert(theme !== undefined);
|
|
||||||
return theme.types;
|
|
||||||
})()
|
|
||||||
});
|
|
||||||
|
|
||||||
return newMetaInfKeycloakTheme;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
writeMessagePropertiesFiles({
|
|
||||||
getMessageDirPath: ({ themeType }) =>
|
|
||||||
pathJoin(themeVariantDirPath, themeType, "messages"),
|
|
||||||
themeName: themeVariantName
|
|
||||||
});
|
|
||||||
}
|
|
@ -7,7 +7,7 @@ import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPa
|
|||||||
/** Assumes the theme type exists */
|
/** Assumes the theme type exists */
|
||||||
export function readFieldNameUsage(params: {
|
export function readFieldNameUsage(params: {
|
||||||
themeSrcDirPath: string;
|
themeSrcDirPath: string;
|
||||||
themeType: ThemeType;
|
themeType: Exclude<ThemeType, "admin">;
|
||||||
}): string[] {
|
}): string[] {
|
||||||
const { themeSrcDirPath, themeType } = params;
|
const { themeSrcDirPath, themeType } = params;
|
||||||
|
|
||||||
|
@ -2,19 +2,16 @@ import { generateResources } from "./generateResources";
|
|||||||
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { getBuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
|
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
|
||||||
import { buildJars } from "./buildJars";
|
import { buildJars } from "./buildJars";
|
||||||
import type { CliCommandOptions } from "../main";
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import { rmSync } from "../tools/fs.rmSync";
|
import { rmSync } from "../tools/fs.rmSync";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { cliCommandOptions } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const buildContext = getBuildContext({ cliCommandOptions });
|
|
||||||
|
|
||||||
exit_if_maven_not_installed: {
|
exit_if_maven_not_installed: {
|
||||||
let commandOutput: Buffer | undefined = undefined;
|
let commandOutput: Buffer | undefined = undefined;
|
||||||
|
160
src/bin/main.ts
160
src/bin/main.ts
@ -4,8 +4,9 @@ import { termost } from "termost";
|
|||||||
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
||||||
|
import { getBuildContext } from "./shared/buildContext";
|
||||||
|
|
||||||
export type CliCommandOptions = {
|
type CliCommandOptions = {
|
||||||
projectDirPath: string | undefined;
|
projectDirPath: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,17 +70,17 @@ program
|
|||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async cliCommandOptions => {
|
handler: async ({ projectDirPath }) => {
|
||||||
const { command } = await import("./keycloakify");
|
const { command } = await import("./keycloakify");
|
||||||
|
|
||||||
await command({ cliCommandOptions });
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command<{
|
.command<{
|
||||||
port: number | undefined;
|
port: number | undefined;
|
||||||
keycloakVersion: string | undefined;
|
keycloakVersion: string | number | undefined;
|
||||||
realmJsonFilePath: string | undefined;
|
realmJsonFilePath: string | undefined;
|
||||||
}>({
|
}>({
|
||||||
name: "start-keycloak",
|
name: "start-keycloak",
|
||||||
@ -130,10 +131,18 @@ program
|
|||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async cliCommandOptions => {
|
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
|
||||||
const { command } = await import("./start-keycloak");
|
const { command } = await import("./start-keycloak");
|
||||||
|
|
||||||
await command({ cliCommandOptions });
|
await command({
|
||||||
|
buildContext: getBuildContext({ projectDirPath }),
|
||||||
|
cliCommandOptions: {
|
||||||
|
keycloakVersion:
|
||||||
|
keycloakVersion === undefined ? undefined : `${keycloakVersion}`,
|
||||||
|
port,
|
||||||
|
realmJsonFilePath
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -144,10 +153,10 @@ program
|
|||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async cliCommandOptions => {
|
handler: async ({ projectDirPath }) => {
|
||||||
const { command } = await import("./eject-page");
|
const { command } = await import("./eject-page");
|
||||||
|
|
||||||
await command({ cliCommandOptions });
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -158,10 +167,10 @@ program
|
|||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async cliCommandOptions => {
|
handler: async ({ projectDirPath }) => {
|
||||||
const { command } = await import("./add-story");
|
const { command } = await import("./add-story");
|
||||||
|
|
||||||
await command({ cliCommandOptions });
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -172,24 +181,38 @@ program
|
|||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async cliCommandOptions => {
|
handler: async ({ projectDirPath }) => {
|
||||||
const { command } = await import("./initialize-email-theme");
|
const { command } = await import("./initialize-email-theme");
|
||||||
|
|
||||||
await command({ cliCommandOptions });
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command({
|
.command({
|
||||||
name: "initialize-account-theme",
|
name: "initialize-account-theme",
|
||||||
description: "Initialize the account theme."
|
description: "Initialize an Account Single-Page or Multi-Page custom Account UI."
|
||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async cliCommandOptions => {
|
handler: async ({ projectDirPath }) => {
|
||||||
const { command } = await import("./initialize-account-theme");
|
const { command } = await import("./initialize-account-theme");
|
||||||
|
|
||||||
await command({ cliCommandOptions });
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command({
|
||||||
|
name: "initialize-admin-theme",
|
||||||
|
description: "Initialize an Admin Console custom UI."
|
||||||
|
})
|
||||||
|
.task({
|
||||||
|
skip,
|
||||||
|
handler: async ({ projectDirPath }) => {
|
||||||
|
const { command } = await import("./initialize-admin-theme");
|
||||||
|
|
||||||
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -197,14 +220,14 @@ program
|
|||||||
.command({
|
.command({
|
||||||
name: "copy-keycloak-resources-to-public",
|
name: "copy-keycloak-resources-to-public",
|
||||||
description:
|
description:
|
||||||
"(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
|
"(Internal) Copy Keycloak default theme resources to the public directory."
|
||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async cliCommandOptions => {
|
handler: async ({ projectDirPath }) => {
|
||||||
const { command } = await import("./copy-keycloak-resources-to-public");
|
const { command } = await import("./copy-keycloak-resources-to-public");
|
||||||
|
|
||||||
await command({ cliCommandOptions });
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -216,10 +239,107 @@ program
|
|||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async cliCommandOptions => {
|
handler: async ({ projectDirPath }) => {
|
||||||
const { command } = await import("./update-kc-gen");
|
const { command } = await import("./update-kc-gen");
|
||||||
|
|
||||||
await command({ cliCommandOptions });
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command({
|
||||||
|
name: "sync-extensions",
|
||||||
|
description: [
|
||||||
|
"Synchronizes all installed Keycloakify extension modules with your project.",
|
||||||
|
"",
|
||||||
|
"Example of extension modules: '@keycloakify/keycloak-account-ui', '@keycloakify/keycloak-admin-ui', '@keycloakify/keycloak-ui-shared'",
|
||||||
|
"",
|
||||||
|
"This command ensures that:",
|
||||||
|
"- All required files from installed extensions are copied into your project.",
|
||||||
|
"- The copied files are correctly ignored by Git to help you distinguish between your custom source files",
|
||||||
|
" and those provided by the extensions.",
|
||||||
|
"- Peer dependencies declared by the extensions are automatically added to your package.json.",
|
||||||
|
"",
|
||||||
|
"You can safely run this command multiple times. It will only update the files and dependencies if needed,",
|
||||||
|
"ensuring your project stays in sync with the installed extensions.",
|
||||||
|
"",
|
||||||
|
"Typical usage:",
|
||||||
|
"- Should be run as a postinstall script of your project.",
|
||||||
|
""
|
||||||
|
].join("\n")
|
||||||
|
})
|
||||||
|
.task({
|
||||||
|
skip,
|
||||||
|
handler: async ({ projectDirPath }) => {
|
||||||
|
const { command } = await import("./sync-extensions");
|
||||||
|
|
||||||
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command<{
|
||||||
|
path: string;
|
||||||
|
revert: boolean;
|
||||||
|
}>({
|
||||||
|
name: "own",
|
||||||
|
description: [
|
||||||
|
"Manages ownership of auto-generated files provided by Keycloakify extensions.",
|
||||||
|
"",
|
||||||
|
"This command allows you to take ownership of a specific file or directory generated",
|
||||||
|
"by an extension. Once owned, you can freely modify and version-control the file.",
|
||||||
|
"",
|
||||||
|
"You can also use the --revert flag to relinquish ownership and restore the file",
|
||||||
|
"or directory to its original auto-generated state.",
|
||||||
|
"",
|
||||||
|
"For convenience, the exact command to take ownership of any file is included as a comment",
|
||||||
|
"in the header of each extension-generated file.",
|
||||||
|
"",
|
||||||
|
"Examples:",
|
||||||
|
"$ npx keycloakify own --path admin/KcPage.tsx"
|
||||||
|
].join("\n")
|
||||||
|
})
|
||||||
|
.option({
|
||||||
|
key: "path",
|
||||||
|
name: (() => {
|
||||||
|
const long = "path";
|
||||||
|
const short = "t";
|
||||||
|
|
||||||
|
optionsKeys.push(long, short);
|
||||||
|
|
||||||
|
return { long, short };
|
||||||
|
})(),
|
||||||
|
description: [
|
||||||
|
"Specifies the relative path of the file or directory to take ownership of.",
|
||||||
|
"This path should be relative to your theme directory.",
|
||||||
|
"Example: `--path 'admin/KcPage.tsx'`"
|
||||||
|
].join(" ")
|
||||||
|
})
|
||||||
|
.option({
|
||||||
|
key: "revert",
|
||||||
|
name: (() => {
|
||||||
|
const long = "revert";
|
||||||
|
const short = "r";
|
||||||
|
|
||||||
|
optionsKeys.push(long, short);
|
||||||
|
|
||||||
|
return { long, short };
|
||||||
|
})(),
|
||||||
|
description: [
|
||||||
|
"Restores a file or directory to its original auto-generated state,",
|
||||||
|
"removing your ownership claim and reverting any modifications."
|
||||||
|
].join(" "),
|
||||||
|
defaultValue: false
|
||||||
|
})
|
||||||
|
.task({
|
||||||
|
skip,
|
||||||
|
handler: async ({ projectDirPath, path, revert }) => {
|
||||||
|
const { command } = await import("./own");
|
||||||
|
|
||||||
|
await command({
|
||||||
|
buildContext: getBuildContext({ projectDirPath }),
|
||||||
|
cliCommandOptions: { path, isRevert: revert }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
208
src/bin/own.ts
Normal file
208
src/bin/own.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
|
import { getExtensionModuleFileSourceCodeReadyToBeCopied } from "./sync-extensions/getExtensionModuleFileSourceCodeReadyToBeCopied";
|
||||||
|
import type { ExtensionModuleMeta } from "./sync-extensions/extensionModuleMeta";
|
||||||
|
import { command as command_syncExtensions } from "./sync-extensions/sync-extension";
|
||||||
|
import {
|
||||||
|
readManagedGitignoreFile,
|
||||||
|
writeManagedGitignoreFile
|
||||||
|
} from "./sync-extensions/managedGitignoreFile";
|
||||||
|
import { getExtensionModuleMetas } from "./sync-extensions/extensionModuleMeta";
|
||||||
|
import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath";
|
||||||
|
import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path";
|
||||||
|
import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { isInside } from "./tools/isInside";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export async function command(params: {
|
||||||
|
buildContext: BuildContext;
|
||||||
|
cliCommandOptions: {
|
||||||
|
path: string;
|
||||||
|
isRevert: boolean;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
const { buildContext, cliCommandOptions } = params;
|
||||||
|
|
||||||
|
const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
|
||||||
|
|
||||||
|
const { targetFileRelativePathsByExtensionModuleMeta } = await (async () => {
|
||||||
|
const fileOrDirectoryRelativePath = pathRelative(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
getAbsoluteAndInOsFormatPath({
|
||||||
|
cwd: buildContext.themeSrcDirPath,
|
||||||
|
pathIsh: cliCommandOptions.path
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const arr = extensionModuleMetas
|
||||||
|
.map(extensionModuleMeta => ({
|
||||||
|
extensionModuleMeta,
|
||||||
|
fileRelativePaths: extensionModuleMeta.files
|
||||||
|
.map(({ fileRelativePath }) => fileRelativePath)
|
||||||
|
.filter(
|
||||||
|
fileRelativePath =>
|
||||||
|
fileRelativePath === fileOrDirectoryRelativePath ||
|
||||||
|
isInside({
|
||||||
|
dirPath: fileOrDirectoryRelativePath,
|
||||||
|
filePath: fileRelativePath
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
.filter(({ fileRelativePaths }) => fileRelativePaths.length !== 0);
|
||||||
|
|
||||||
|
const targetFileRelativePathsByExtensionModuleMeta = new Map<
|
||||||
|
ExtensionModuleMeta,
|
||||||
|
string[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const { extensionModuleMeta, fileRelativePaths } of arr) {
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta.set(
|
||||||
|
extensionModuleMeta,
|
||||||
|
fileRelativePaths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { targetFileRelativePathsByExtensionModuleMeta };
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (targetFileRelativePathsByExtensionModuleMeta.size === 0) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
"There is no Keycloakify extension modules files matching the provided path."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ownedFilesRelativePaths: ownedFilesRelativePaths_current } =
|
||||||
|
await readManagedGitignoreFile({
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
await (cliCommandOptions.isRevert ? command_revert : command_own)({
|
||||||
|
extensionModuleMetas,
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta,
|
||||||
|
ownedFilesRelativePaths_current,
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params_subcommands = {
|
||||||
|
extensionModuleMetas: ExtensionModuleMeta[];
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta: Map<ExtensionModuleMeta, string[]>;
|
||||||
|
ownedFilesRelativePaths_current: string[];
|
||||||
|
buildContext: BuildContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function command_own(params: Params_subcommands) {
|
||||||
|
const {
|
||||||
|
extensionModuleMetas,
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta,
|
||||||
|
ownedFilesRelativePaths_current,
|
||||||
|
buildContext
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
await writeManagedGitignoreFile({
|
||||||
|
buildContext,
|
||||||
|
extensionModuleMetas,
|
||||||
|
ownedFilesRelativePaths: [
|
||||||
|
...ownedFilesRelativePaths_current,
|
||||||
|
...Array.from(targetFileRelativePathsByExtensionModuleMeta.values())
|
||||||
|
.flat()
|
||||||
|
.filter(
|
||||||
|
fileRelativePath =>
|
||||||
|
!ownedFilesRelativePaths_current.includes(fileRelativePath)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const writeActions: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
for (const [
|
||||||
|
extensionModuleMeta,
|
||||||
|
fileRelativePaths
|
||||||
|
] of targetFileRelativePathsByExtensionModuleMeta.entries()) {
|
||||||
|
const extensionModuleDirPath = await getInstalledModuleDirPath({
|
||||||
|
moduleName: extensionModuleMeta.moduleName,
|
||||||
|
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const fileRelativePath of fileRelativePaths) {
|
||||||
|
if (ownedFilesRelativePaths_current.includes(fileRelativePath)) {
|
||||||
|
console.log(
|
||||||
|
chalk.grey(`You already have ownership over '${fileRelativePath}'.`)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeActions.push(async () => {
|
||||||
|
const sourceCode = await getExtensionModuleFileSourceCodeReadyToBeCopied({
|
||||||
|
buildContext,
|
||||||
|
fileRelativePath,
|
||||||
|
isOwnershipAction: true,
|
||||||
|
extensionModuleName: extensionModuleMeta.moduleName,
|
||||||
|
extensionModuleDirPath,
|
||||||
|
extensionModuleVersion: extensionModuleMeta.version
|
||||||
|
});
|
||||||
|
|
||||||
|
await fsPr.writeFile(
|
||||||
|
pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
|
||||||
|
sourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(chalk.green(`Ownership over '${fileRelativePath}' claimed.`));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeActions.length === 0) {
|
||||||
|
console.log(chalk.yellow("No new file claimed."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(writeActions.map(action => action()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function command_revert(params: Params_subcommands) {
|
||||||
|
const {
|
||||||
|
extensionModuleMetas,
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta,
|
||||||
|
ownedFilesRelativePaths_current,
|
||||||
|
buildContext
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const ownedFilesRelativePaths_toRemove = Array.from(
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta.values()
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
.filter(fileRelativePath => {
|
||||||
|
if (!ownedFilesRelativePaths_current.includes(fileRelativePath)) {
|
||||||
|
console.log(
|
||||||
|
chalk.grey(`Ownership over '${fileRelativePath}' wasn't claimed.`)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
chalk.green(`Ownership over '${fileRelativePath}' relinquished.`)
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ownedFilesRelativePaths_toRemove.length === 0) {
|
||||||
|
console.log(chalk.yellow("No file relinquished."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeManagedGitignoreFile({
|
||||||
|
buildContext,
|
||||||
|
extensionModuleMetas,
|
||||||
|
ownedFilesRelativePaths: ownedFilesRelativePaths_current.filter(
|
||||||
|
fileRelativePath =>
|
||||||
|
!ownedFilesRelativePaths_toRemove.includes(fileRelativePath)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
await command_syncExtensions({ buildContext });
|
||||||
|
}
|
@ -3,7 +3,7 @@ export type KeycloakVersionRange =
|
|||||||
| KeycloakVersionRange.WithoutAccountV1Theme;
|
| KeycloakVersionRange.WithoutAccountV1Theme;
|
||||||
|
|
||||||
export namespace KeycloakVersionRange {
|
export namespace KeycloakVersionRange {
|
||||||
export type WithoutAccountV1Theme = "21-and-below" | "22-and-above";
|
export type WithoutAccountV1Theme = "22-to-25" | "all-other-versions";
|
||||||
|
|
||||||
export type WithAccountV1Theme = "21-and-below" | "23" | "24" | "25-and-above";
|
export type WithAccountV1Theme = "21-and-below" | "23" | "24" | "25" | "26-and-above";
|
||||||
}
|
}
|
||||||
|
70
src/bin/shared/addSyncExtensionsToPostinstallScript.ts
Normal file
70
src/bin/shared/addSyncExtensionsToPostinstallScript.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "./buildContext";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
projectDirPath: string;
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export function addSyncExtensionsToPostinstallScript(params: {
|
||||||
|
parsedPackageJson: { scripts?: Record<string, string | undefined> };
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}) {
|
||||||
|
const { parsedPackageJson, buildContext } = params;
|
||||||
|
|
||||||
|
const cmd_base = "keycloakify sync-extensions";
|
||||||
|
|
||||||
|
const projectCliOptionValue = (() => {
|
||||||
|
const packageJsonDirPath = pathDirname(buildContext.packageJsonFilePath);
|
||||||
|
|
||||||
|
const relativePath = pathRelative(
|
||||||
|
packageJsonDirPath,
|
||||||
|
buildContext.projectDirPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relativePath === "") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return relativePath.split(pathSep).join("/");
|
||||||
|
})();
|
||||||
|
|
||||||
|
const generateCmd = (params: { cmd_preexisting: string | undefined }) => {
|
||||||
|
const { cmd_preexisting } = params;
|
||||||
|
|
||||||
|
let cmd = cmd_preexisting === undefined ? "" : `${cmd_preexisting} && `;
|
||||||
|
|
||||||
|
cmd += cmd_base;
|
||||||
|
|
||||||
|
if (projectCliOptionValue !== undefined) {
|
||||||
|
cmd += ` -p ${projectCliOptionValue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const scripts = (parsedPackageJson.scripts ??= {});
|
||||||
|
|
||||||
|
for (const scriptName of ["postinstall", "prepare"]) {
|
||||||
|
const cmd_preexisting = scripts[scriptName];
|
||||||
|
|
||||||
|
if (cmd_preexisting === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cmd_preexisting.includes(cmd_base)) {
|
||||||
|
scripts[scriptName] = generateCmd({ cmd_preexisting });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedPackageJson.scripts = {
|
||||||
|
postinstall: generateCmd({ cmd_preexisting: undefined }),
|
||||||
|
...parsedPackageJson.scripts
|
||||||
|
};
|
||||||
|
}
|
@ -7,10 +7,9 @@ import {
|
|||||||
dirname as pathDirname
|
dirname as pathDirname
|
||||||
} from "path";
|
} from "path";
|
||||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||||
import type { CliCommandOptions } from "../main";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals, is } from "tsafe/assert";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import {
|
import {
|
||||||
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
||||||
@ -19,12 +18,11 @@ import {
|
|||||||
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
||||||
import { exclude } from "tsafe";
|
import { exclude } from "tsafe";
|
||||||
import { crawl } from "../tools/crawl";
|
import { crawl } from "../tools/crawl";
|
||||||
import { THEME_TYPES } from "./constants";
|
import { THEME_TYPES, KEYCLOAK_THEME, type ThemeType } from "./constants";
|
||||||
import { objectEntries } from "tsafe/objectEntries";
|
import { objectEntries } from "tsafe/objectEntries";
|
||||||
import { type ThemeType } from "./constants";
|
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
|
import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
|
||||||
|
|
||||||
export type BuildContext = {
|
export type BuildContext = {
|
||||||
themeVersion: string;
|
themeVersion: string;
|
||||||
@ -42,16 +40,21 @@ export type BuildContext = {
|
|||||||
* In this case the urlPathname will be "/my-app/" */
|
* In this case the urlPathname will be "/my-app/" */
|
||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
assetsDirPath: string;
|
assetsDirPath: string;
|
||||||
fetchOptions: ProxyFetchOptions;
|
fetchOptions: FetchOptionsLike;
|
||||||
kcContextExclusionsFtlCode: string | undefined;
|
kcContextExclusionsFtlCode: string | undefined;
|
||||||
environmentVariables: { name: string; default: string }[];
|
environmentVariables: { name: string; default: string }[];
|
||||||
themeSrcDirPath: string;
|
themeSrcDirPath: string;
|
||||||
implementedThemeTypes: {
|
implementedThemeTypes: {
|
||||||
login: { isImplemented: boolean };
|
login:
|
||||||
email: { isImplemented: boolean };
|
| { isImplemented: true }
|
||||||
|
| { isImplemented: false; isImplemented_native: boolean };
|
||||||
|
email: { isImplemented: false; isImplemented_native: boolean };
|
||||||
account:
|
account:
|
||||||
| { isImplemented: false }
|
| { isImplemented: false; isImplemented_native: boolean }
|
||||||
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
||||||
|
admin:
|
||||||
|
| { isImplemented: true }
|
||||||
|
| { isImplemented: false; isImplemented_native: boolean };
|
||||||
};
|
};
|
||||||
packageJsonFilePath: string;
|
packageJsonFilePath: string;
|
||||||
bundler: "vite" | "webpack";
|
bundler: "vite" | "webpack";
|
||||||
@ -128,14 +131,12 @@ export type ResolvedViteConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getBuildContext(params: {
|
export function getBuildContext(params: {
|
||||||
cliCommandOptions: CliCommandOptions;
|
projectDirPath: string | undefined;
|
||||||
}): BuildContext {
|
}): BuildContext {
|
||||||
const { cliCommandOptions } = params;
|
|
||||||
|
|
||||||
const projectDirPath =
|
const projectDirPath =
|
||||||
cliCommandOptions.projectDirPath !== undefined
|
params.projectDirPath !== undefined
|
||||||
? getAbsoluteAndInOsFormatPath({
|
? getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: cliCommandOptions.projectDirPath,
|
pathIsh: params.projectDirPath,
|
||||||
cwd: process.cwd()
|
cwd: process.cwd()
|
||||||
})
|
})
|
||||||
: process.cwd();
|
: process.cwd();
|
||||||
@ -148,7 +149,10 @@ export function getBuildContext(params: {
|
|||||||
returnedPathsType: "relative to dirPath"
|
returnedPathsType: "relative to dirPath"
|
||||||
})
|
})
|
||||||
.map(fileRelativePath => {
|
.map(fileRelativePath => {
|
||||||
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
|
for (const themeSrcDirBasename of [
|
||||||
|
KEYCLOAK_THEME,
|
||||||
|
KEYCLOAK_THEME.replace(/-/g, "_")
|
||||||
|
]) {
|
||||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||||
if (split.length === 2) {
|
if (split.length === 2) {
|
||||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||||
@ -174,7 +178,7 @@ export function getBuildContext(params: {
|
|||||||
[
|
[
|
||||||
`Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
|
`Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
|
||||||
`Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`,
|
`Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`,
|
||||||
`If you are collocating your Keycloak theme with your app you must have a directory named 'keycloak-theme' or 'keycloak_theme' in your 'src' directory`
|
`If you are collocating your Keycloak theme with your app you must have a directory named '${KEYCLOAK_THEME}' or '${KEYCLOAK_THEME.replace(/-/g, "_")}' in your 'src' directory`
|
||||||
].join("\n")
|
].join("\n")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -240,8 +244,7 @@ export function getBuildContext(params: {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
parsedPackageJson.dependencies?.keycloakify === undefined &&
|
parsedPackageJson.dependencies?.keycloakify === undefined &&
|
||||||
parsedPackageJson.devDependencies?.keycloakify === undefined &&
|
parsedPackageJson.devDependencies?.keycloakify === undefined
|
||||||
parsedPackageJson.name !== "keycloakify" // NOTE: For local storybook build
|
|
||||||
) {
|
) {
|
||||||
break success;
|
break success;
|
||||||
}
|
}
|
||||||
@ -277,7 +280,8 @@ export function getBuildContext(params: {
|
|||||||
"21-and-below": z.union([z.boolean(), z.string()]),
|
"21-and-below": z.union([z.boolean(), z.string()]),
|
||||||
"23": z.union([z.boolean(), z.string()]),
|
"23": z.union([z.boolean(), z.string()]),
|
||||||
"24": z.union([z.boolean(), z.string()]),
|
"24": z.union([z.boolean(), z.string()]),
|
||||||
"25-and-above": z.union([z.boolean(), z.string()])
|
"25": z.union([z.boolean(), z.string()]),
|
||||||
|
"26-and-above": z.union([z.boolean(), z.string()])
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
@ -298,8 +302,8 @@ export function getBuildContext(params: {
|
|||||||
]),
|
]),
|
||||||
keycloakVersionTargets: z
|
keycloakVersionTargets: z
|
||||||
.object({
|
.object({
|
||||||
"21-and-below": z.union([z.boolean(), z.string()]),
|
"22-to-25": z.union([z.boolean(), z.string()]),
|
||||||
"22-and-above": z.union([z.boolean(), z.string()])
|
"all-other-versions": z.union([z.boolean(), z.string()])
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
@ -434,24 +438,68 @@ export function getBuildContext(params: {
|
|||||||
assert<Equals<typeof bundler, never>>(false);
|
assert<Equals<typeof bundler, never>>(false);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = {
|
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = (() => {
|
||||||
login: {
|
const getIsNative = (dirPath: string) =>
|
||||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "login"))
|
fs.existsSync(pathJoin(dirPath, "theme.properties"));
|
||||||
},
|
|
||||||
email: {
|
|
||||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "email"))
|
|
||||||
},
|
|
||||||
account: (() => {
|
|
||||||
if (buildOptions.accountThemeImplementation === "none") {
|
|
||||||
return { isImplemented: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isImplemented: true,
|
login: (() => {
|
||||||
type: buildOptions.accountThemeImplementation
|
const dirPath = pathJoin(themeSrcDirPath, "login");
|
||||||
};
|
|
||||||
})()
|
if (!fs.existsSync(dirPath)) {
|
||||||
};
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsNative(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isImplemented: true };
|
||||||
|
})(),
|
||||||
|
email: (() => {
|
||||||
|
const dirPath = pathJoin(themeSrcDirPath, "email");
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath) || !getIsNative(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isImplemented: false, isImplemented_native: true };
|
||||||
|
})(),
|
||||||
|
account: (() => {
|
||||||
|
const dirPath = pathJoin(themeSrcDirPath, "account");
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsNative(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildOptions.accountThemeImplementation === "none") {
|
||||||
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isImplemented: true,
|
||||||
|
type: buildOptions.accountThemeImplementation
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
admin: (() => {
|
||||||
|
const dirPath = pathJoin(themeSrcDirPath, "admin");
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsNative(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isImplemented: true };
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
implementedThemeTypes.account.isImplemented &&
|
implementedThemeTypes.account.isImplemented &&
|
||||||
@ -470,26 +518,53 @@ export function getBuildContext(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const themeNames = ((): [string, ...string[]] => {
|
const themeNames = ((): [string, ...string[]] => {
|
||||||
if (buildOptions.themeName === undefined) {
|
const themeNames = ((): [string, ...string[]] => {
|
||||||
return parsedPackageJson.name === undefined
|
if (buildOptions.themeName === undefined) {
|
||||||
? ["keycloakify"]
|
return parsedPackageJson.name === undefined
|
||||||
: [
|
? ["keycloakify"]
|
||||||
parsedPackageJson.name
|
: [
|
||||||
.replace(/^@(.*)/, "$1")
|
parsedPackageJson.name
|
||||||
.split("/")
|
.replace(/^@(.*)/, "$1")
|
||||||
.join("-")
|
.split("/")
|
||||||
];
|
.join("-")
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof buildOptions.themeName === "string") {
|
||||||
|
return [buildOptions.themeName];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
|
||||||
|
|
||||||
|
assert(mainThemeName !== undefined);
|
||||||
|
|
||||||
|
return [mainThemeName, ...themeVariantNames];
|
||||||
|
})();
|
||||||
|
|
||||||
|
for (const themeName of themeNames) {
|
||||||
|
if (!/^[a-zA-Z0-9_-]+$/.test(themeName)) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(
|
||||||
|
[
|
||||||
|
`Invalid theme name: ${themeName}`,
|
||||||
|
`Theme names should only contain letters, numbers, and "_" or "-"`
|
||||||
|
].join(" ")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof buildOptions.themeName === "string") {
|
return themeNames;
|
||||||
return [buildOptions.themeName];
|
})();
|
||||||
|
|
||||||
|
const relativePathsCwd = (() => {
|
||||||
|
switch (bundler) {
|
||||||
|
case "vite":
|
||||||
|
return projectDirPath;
|
||||||
|
case "webpack":
|
||||||
|
return pathDirname(packageJsonFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
|
|
||||||
|
|
||||||
assert(mainThemeName !== undefined);
|
|
||||||
|
|
||||||
return [mainThemeName, ...themeVariantNames];
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const projectBuildDirPath = (() => {
|
const projectBuildDirPath = (() => {
|
||||||
@ -503,7 +578,7 @@ export function getBuildContext(params: {
|
|||||||
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
|
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
|
||||||
return getAbsoluteAndInOsFormatPath({
|
return getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
|
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
|
||||||
cwd: projectDirPath
|
cwd: relativePathsCwd
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -547,7 +622,7 @@ export function getBuildContext(params: {
|
|||||||
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
|
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
|
||||||
return getAbsoluteAndInOsFormatPath({
|
return getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: buildOptions.keycloakifyBuildDirPath,
|
pathIsh: buildOptions.keycloakifyBuildDirPath,
|
||||||
cwd: projectDirPath
|
cwd: relativePathsCwd
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,7 +651,7 @@ export function getBuildContext(params: {
|
|||||||
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
|
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
|
||||||
return getAbsoluteAndInOsFormatPath({
|
return getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: parsedPackageJson.keycloakify.publicDirPath,
|
pathIsh: parsedPackageJson.keycloakify.publicDirPath,
|
||||||
cwd: projectDirPath
|
cwd: relativePathsCwd
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -648,7 +723,7 @@ export function getBuildContext(params: {
|
|||||||
pathIsh:
|
pathIsh:
|
||||||
parsedPackageJson.keycloakify
|
parsedPackageJson.keycloakify
|
||||||
.staticDirPathInProjectBuildDirPath,
|
.staticDirPathInProjectBuildDirPath,
|
||||||
cwd: projectBuildDirPath
|
cwd: relativePathsCwd
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -749,7 +824,11 @@ export function getBuildContext(params: {
|
|||||||
return "24" as const;
|
return "24" as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "25-and-above" as const;
|
if (buildForKeycloakMajorVersionNumber === 25) {
|
||||||
|
return "25" as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "26-and-above" as const;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
assert<
|
assert<
|
||||||
@ -762,11 +841,14 @@ export function getBuildContext(params: {
|
|||||||
return keycloakVersionRange;
|
return keycloakVersionRange;
|
||||||
} else {
|
} else {
|
||||||
const keycloakVersionRange = (() => {
|
const keycloakVersionRange = (() => {
|
||||||
if (buildForKeycloakMajorVersionNumber <= 21) {
|
if (
|
||||||
return "21-and-below" as const;
|
buildForKeycloakMajorVersionNumber <= 21 ||
|
||||||
|
buildForKeycloakMajorVersionNumber >= 26
|
||||||
|
) {
|
||||||
|
return "all-other-versions" as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "22-and-above" as const;
|
return "22-to-25" as const;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
assert<
|
assert<
|
||||||
@ -784,6 +866,12 @@ export function getBuildContext(params: {
|
|||||||
use_custom_jar_basename: {
|
use_custom_jar_basename: {
|
||||||
const { keycloakVersionTargets } = buildOptions;
|
const { keycloakVersionTargets } = buildOptions;
|
||||||
|
|
||||||
|
assert(
|
||||||
|
is<Record<KeycloakVersionRange, string | boolean>>(
|
||||||
|
keycloakVersionTargets
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (keycloakVersionTargets === undefined) {
|
if (keycloakVersionTargets === undefined) {
|
||||||
break use_custom_jar_basename;
|
break use_custom_jar_basename;
|
||||||
}
|
}
|
||||||
@ -828,7 +916,8 @@ export function getBuildContext(params: {
|
|||||||
"21-and-below",
|
"21-and-below",
|
||||||
"23",
|
"23",
|
||||||
"24",
|
"24",
|
||||||
"25-and-above"
|
"25",
|
||||||
|
"26-and-above"
|
||||||
] as const) {
|
] as const) {
|
||||||
assert<
|
assert<
|
||||||
Equals<
|
Equals<
|
||||||
@ -844,8 +933,8 @@ export function getBuildContext(params: {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const keycloakVersionRange of [
|
for (const keycloakVersionRange of [
|
||||||
"21-and-below",
|
"22-to-25",
|
||||||
"22-and-above"
|
"all-other-versions"
|
||||||
] as const) {
|
] as const) {
|
||||||
assert<
|
assert<
|
||||||
Equals<
|
Equals<
|
||||||
@ -871,7 +960,17 @@ export function getBuildContext(params: {
|
|||||||
const jarTargets: BuildContext["jarTargets"] = [];
|
const jarTargets: BuildContext["jarTargets"] = [];
|
||||||
|
|
||||||
for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(
|
for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(
|
||||||
buildOptions.keycloakVersionTargets
|
(() => {
|
||||||
|
const { keycloakVersionTargets } = buildOptions;
|
||||||
|
|
||||||
|
assert(
|
||||||
|
is<Record<KeycloakVersionRange, string | boolean>>(
|
||||||
|
keycloakVersionTargets
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return keycloakVersionTargets;
|
||||||
|
})()
|
||||||
)) {
|
)) {
|
||||||
if (jarNameOrBoolean === false) {
|
if (jarNameOrBoolean === false) {
|
||||||
continue;
|
continue;
|
||||||
@ -952,7 +1051,7 @@ export function getBuildContext(params: {
|
|||||||
type: "path",
|
type: "path",
|
||||||
path: getAbsoluteAndInOsFormatPath({
|
path: getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: urlOrPath,
|
pathIsh: urlOrPath,
|
||||||
cwd: projectDirPath
|
cwd: relativePathsCwd
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -962,7 +1061,7 @@ export function getBuildContext(params: {
|
|||||||
? undefined
|
? undefined
|
||||||
: getAbsoluteAndInOsFormatPath({
|
: getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
|
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
|
||||||
cwd: projectDirPath
|
cwd: relativePathsCwd
|
||||||
}),
|
}),
|
||||||
port: buildOptions.startKeycloakOptions?.port
|
port: buildOptions.startKeycloakOptions?.port
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,14 @@ export const WELL_KNOWN_DIRECTORY_BASE_NAME = {
|
|||||||
DIST: "dist"
|
DIST: "dist"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const THEME_TYPES = ["login", "account"] as const;
|
export const THEME_TYPES = ["login", "account", "admin"] as const;
|
||||||
|
|
||||||
export type ThemeType = (typeof THEME_TYPES)[number];
|
export type ThemeType = (typeof THEME_TYPES)[number];
|
||||||
|
|
||||||
export const VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES = {
|
export const VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES = {
|
||||||
RUN_POST_BUILD_SCRIPT: "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
|
RUN_POST_BUILD_SCRIPT: "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
|
||||||
RESOLVE_VITE_CONFIG: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
|
RESOLVE_VITE_CONFIG: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG",
|
||||||
|
READ_KC_CONTEXT_FROM_URL: "KEYCLOAKIFY_READ_KC_CONTEXT_FROM_URL"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME =
|
export const BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME =
|
||||||
@ -71,3 +72,18 @@ export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number];
|
|||||||
export const CONTAINER_NAME = "keycloak-keycloakify";
|
export const CONTAINER_NAME = "keycloak-keycloakify";
|
||||||
|
|
||||||
export const FALLBACK_LANGUAGE_TAG = "en";
|
export const FALLBACK_LANGUAGE_TAG = "en";
|
||||||
|
|
||||||
|
export const CUSTOM_HANDLER_ENV_NAMES = {
|
||||||
|
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
|
||||||
|
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KEYCLOAK_THEME = "keycloak-theme";
|
||||||
|
|
||||||
|
export const KEYCLOAKIFY_SPA_DEV_SERVER_PORT = "KEYCLOAKIFY_SPA_DEV_SERVER_PORT";
|
||||||
|
|
||||||
|
export const KEYCLOAKIFY_LOGGING_VERSION = "1.0.3";
|
||||||
|
|
||||||
|
export const KEYCLOAKIFY_LOGIN_JAR_BASENAME = `keycloakify-logging-${KEYCLOAKIFY_LOGGING_VERSION}.jar`;
|
||||||
|
|
||||||
|
export const TEST_APP_URL = "https://my-theme.keycloakify.dev";
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
|
||||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../shared/constants";
|
|
||||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import { rmSync } from "../tools/fs.rmSync";
|
|
||||||
import type { BuildContext } from "./buildContext";
|
|
||||||
import { transformCodebase } from "../tools/transformCodebase";
|
|
||||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
|
||||||
|
|
||||||
export type BuildContextLike = {
|
|
||||||
publicDirPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export function copyKeycloakResourcesToPublic(params: {
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
}) {
|
|
||||||
const { buildContext } = params;
|
|
||||||
|
|
||||||
const destDirPath = pathJoin(
|
|
||||||
buildContext.publicDirPath,
|
|
||||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
|
||||||
);
|
|
||||||
|
|
||||||
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
|
|
||||||
|
|
||||||
const keycloakifyBuildinfoRaw = JSON.stringify(
|
|
||||||
{
|
|
||||||
keycloakifyVersion: readThisNpmPackageVersion()
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
);
|
|
||||||
|
|
||||||
skip_if_already_done: {
|
|
||||||
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
|
|
||||||
break skip_if_already_done;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keycloakifyBuildinfoRaw_previousRun = fs
|
|
||||||
.readFileSync(keycloakifyBuildinfoFilePath)
|
|
||||||
.toString("utf8");
|
|
||||||
|
|
||||||
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
|
|
||||||
break skip_if_already_done;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rmSync(destDirPath, { force: true, recursive: true });
|
|
||||||
|
|
||||||
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
|
|
||||||
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
|
|
||||||
force: true,
|
|
||||||
recursive: true
|
|
||||||
});
|
|
||||||
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
|
|
||||||
force: true,
|
|
||||||
recursive: true
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.mkdirSync(destDirPath, { recursive: true });
|
|
||||||
|
|
||||||
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: pathJoin(
|
|
||||||
getThisCodebaseRootDirPath(),
|
|
||||||
"res",
|
|
||||||
"public",
|
|
||||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
|
||||||
),
|
|
||||||
destDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(destDirPath, "README.txt"),
|
|
||||||
Buffer.from(
|
|
||||||
// prettier-ignore
|
|
||||||
[
|
|
||||||
"This directory is only used in dev mode by Keycloakify",
|
|
||||||
"It won't be included in your final build.",
|
|
||||||
"Do not modify anything in this directory.",
|
|
||||||
].join("\n")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
keycloakifyBuildinfoFilePath,
|
|
||||||
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
|
|
||||||
);
|
|
||||||
}
|
|
43
src/bin/shared/customHandler.ts
Normal file
43
src/bin/shared/customHandler.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "./buildContext";
|
||||||
|
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
|
||||||
|
|
||||||
|
export const BIN_NAME = "_keycloakify-custom-handler";
|
||||||
|
|
||||||
|
export const NOT_IMPLEMENTED_EXIT_CODE = 78;
|
||||||
|
|
||||||
|
export type CommandName =
|
||||||
|
| "update-kc-gen"
|
||||||
|
| "eject-page"
|
||||||
|
| "add-story"
|
||||||
|
| "initialize-account-theme"
|
||||||
|
| "initialize-admin-theme"
|
||||||
|
| "initialize-admin-theme"
|
||||||
|
| "initialize-email-theme"
|
||||||
|
| "copy-keycloak-resources-to-public";
|
||||||
|
|
||||||
|
export type ApiVersion = "v1";
|
||||||
|
|
||||||
|
export function readParams(params: { apiVersion: ApiVersion }) {
|
||||||
|
const { apiVersion } = params;
|
||||||
|
|
||||||
|
assert(apiVersion === "v1");
|
||||||
|
|
||||||
|
const commandName = (() => {
|
||||||
|
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME];
|
||||||
|
|
||||||
|
assert(envValue !== undefined);
|
||||||
|
|
||||||
|
return envValue as CommandName;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const buildContext = (() => {
|
||||||
|
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT];
|
||||||
|
|
||||||
|
assert(envValue !== undefined);
|
||||||
|
|
||||||
|
return JSON.parse(envValue) as BuildContext;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return { commandName, buildContext };
|
||||||
|
}
|
50
src/bin/shared/customHandler_delegate.ts
Normal file
50
src/bin/shared/customHandler_delegate.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "./buildContext";
|
||||||
|
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
|
||||||
|
import {
|
||||||
|
NOT_IMPLEMENTED_EXIT_CODE,
|
||||||
|
type CommandName,
|
||||||
|
BIN_NAME,
|
||||||
|
ApiVersion
|
||||||
|
} from "./customHandler";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { getNodeModulesBinDirPath } from "../tools/nodeModulesBinDirPath";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
assert<Equals<ApiVersion, "v1">>();
|
||||||
|
|
||||||
|
export async function maybeDelegateCommandToCustomHandler(params: {
|
||||||
|
commandName: CommandName;
|
||||||
|
buildContext: BuildContext;
|
||||||
|
}): Promise<{ hasBeenHandled: boolean }> {
|
||||||
|
const { commandName, buildContext } = params;
|
||||||
|
|
||||||
|
const nodeModulesBinDirPath = await getNodeModulesBinDirPath({
|
||||||
|
packageJsonFilePath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
||||||
|
return { hasBeenHandled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
child_process.execSync(`npx ${BIN_NAME}`, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME]: commandName,
|
||||||
|
[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT]: JSON.stringify(buildContext)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
const status = error.status;
|
||||||
|
|
||||||
|
if (status === NOT_IMPLEMENTED_EXIT_CODE) {
|
||||||
|
return { hasBeenHandled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hasBeenHandled: true };
|
||||||
|
}
|
36
src/bin/shared/exitIfUncommittedChanges.ts
Normal file
36
src/bin/shared/exitIfUncommittedChanges.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import child_process from "child_process";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export function exitIfUncommittedChanges(params: { projectDirPath: string }) {
|
||||||
|
const { projectDirPath } = params;
|
||||||
|
|
||||||
|
let hasUncommittedChanges: boolean | undefined = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
hasUncommittedChanges =
|
||||||
|
child_process
|
||||||
|
.execSync(`git status --porcelain`, {
|
||||||
|
cwd: projectDirPath
|
||||||
|
})
|
||||||
|
.toString()
|
||||||
|
.trim() !== "";
|
||||||
|
} catch {
|
||||||
|
// Probably not a git repository
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasUncommittedChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
[
|
||||||
|
chalk.red(
|
||||||
|
"Please commit or stash your changes before running this command.\n"
|
||||||
|
),
|
||||||
|
"This command will modify your project's files so it's better to have a clean working directory",
|
||||||
|
"so that you can easily see what has been changed and revert if needed."
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
@ -1,175 +0,0 @@
|
|||||||
import { assert, type Equals } from "tsafe/assert";
|
|
||||||
import { id } from "tsafe/id";
|
|
||||||
import type { BuildContext } from "./buildContext";
|
|
||||||
import * as fs from "fs/promises";
|
|
||||||
import { join as pathJoin } from "path";
|
|
||||||
import { existsAsync } from "../tools/fs.existsAsync";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export type BuildContextLike = {
|
|
||||||
projectDirPath: string;
|
|
||||||
themeNames: string[];
|
|
||||||
environmentVariables: { name: string; default: string }[];
|
|
||||||
themeSrcDirPath: string;
|
|
||||||
implementedThemeTypes: Pick<
|
|
||||||
BuildContext["implementedThemeTypes"],
|
|
||||||
"login" | "account"
|
|
||||||
>;
|
|
||||||
packageJsonFilePath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export async function generateKcGenTs(params: {
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
}): Promise<void> {
|
|
||||||
const { buildContext } = params;
|
|
||||||
|
|
||||||
const isReactProject: boolean = await (async () => {
|
|
||||||
const parsedPackageJson = await (async () => {
|
|
||||||
type ParsedPackageJson = {
|
|
||||||
dependencies?: Record<string, string>;
|
|
||||||
devDependencies?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const zParsedPackageJson = (() => {
|
|
||||||
type TargetType = ParsedPackageJson;
|
|
||||||
|
|
||||||
const zTargetType = z.object({
|
|
||||||
dependencies: z.record(z.string()).optional(),
|
|
||||||
devDependencies: z.record(z.string()).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
|
||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return zParsedPackageJson.parse(
|
|
||||||
JSON.parse(
|
|
||||||
(await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
{
|
|
||||||
...parsedPackageJson.dependencies,
|
|
||||||
...parsedPackageJson.devDependencies
|
|
||||||
}.react !== undefined
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const filePath = pathJoin(
|
|
||||||
buildContext.themeSrcDirPath,
|
|
||||||
`kc.gen.ts${isReactProject ? "x" : ""}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentContent = (await existsAsync(filePath))
|
|
||||||
? await fs.readFile(filePath)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
|
|
||||||
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
|
|
||||||
|
|
||||||
const newContent = Buffer.from(
|
|
||||||
[
|
|
||||||
`/* prettier-ignore-start */`,
|
|
||||||
``,
|
|
||||||
`/* eslint-disable */`,
|
|
||||||
``,
|
|
||||||
`// @ts-nocheck`,
|
|
||||||
``,
|
|
||||||
`// noinspection JSUnusedGlobalSymbols`,
|
|
||||||
``,
|
|
||||||
`// This file is auto-generated by Keycloakify`,
|
|
||||||
``,
|
|
||||||
isReactProject && `import { lazy, Suspense, type ReactNode } from "react";`,
|
|
||||||
``,
|
|
||||||
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
|
|
||||||
``,
|
|
||||||
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
|
|
||||||
``,
|
|
||||||
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
|
|
||||||
``,
|
|
||||||
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
|
|
||||||
``,
|
|
||||||
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
|
|
||||||
Object.fromEntries(
|
|
||||||
buildContext.environmentVariables.map(
|
|
||||||
({ name, default: defaultValue }) => [name, defaultValue]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)};`,
|
|
||||||
``,
|
|
||||||
`export type KcContext =`,
|
|
||||||
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
|
|
||||||
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
|
|
||||||
` ;`,
|
|
||||||
``,
|
|
||||||
`declare global {`,
|
|
||||||
` interface Window {`,
|
|
||||||
` kcContext?: KcContext;`,
|
|
||||||
` }`,
|
|
||||||
`}`,
|
|
||||||
``,
|
|
||||||
...(!isReactProject
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
hasLoginTheme &&
|
|
||||||
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
|
|
||||||
hasAccountTheme &&
|
|
||||||
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
|
|
||||||
``,
|
|
||||||
`export function KcPage(`,
|
|
||||||
` props: {`,
|
|
||||||
` kcContext: KcContext;`,
|
|
||||||
` fallback?: ReactNode;`,
|
|
||||||
` }`,
|
|
||||||
`) {`,
|
|
||||||
` const { kcContext, fallback } = props;`,
|
|
||||||
` return (`,
|
|
||||||
` <Suspense fallback={fallback}>`,
|
|
||||||
` {(() => {`,
|
|
||||||
` switch (kcContext.themeType) {`,
|
|
||||||
hasLoginTheme &&
|
|
||||||
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
|
|
||||||
hasAccountTheme &&
|
|
||||||
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
|
|
||||||
` }`,
|
|
||||||
` })()}`,
|
|
||||||
` </Suspense>`,
|
|
||||||
` );`,
|
|
||||||
`}`
|
|
||||||
]),
|
|
||||||
``,
|
|
||||||
`/* prettier-ignore-end */`,
|
|
||||||
``
|
|
||||||
]
|
|
||||||
.filter(item => typeof item === "string")
|
|
||||||
.join("\n"),
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentContent !== undefined && currentContent.equals(newContent)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(filePath, newContent);
|
|
||||||
|
|
||||||
delete_legacy_file: {
|
|
||||||
if (!isReactProject) {
|
|
||||||
break delete_legacy_file;
|
|
||||||
}
|
|
||||||
|
|
||||||
const legacyFilePath = filePath.replace(/tsx$/, "ts");
|
|
||||||
|
|
||||||
if (!(await existsAsync(legacyFilePath))) {
|
|
||||||
break delete_legacy_file;
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.unlink(legacyFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,201 +0,0 @@
|
|||||||
import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag";
|
|
||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
import type { ReturnType } from "tsafe";
|
|
||||||
import type { Param0 } from "tsafe";
|
|
||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
|
||||||
import { id } from "tsafe/id";
|
|
||||||
import type { SemVer } from "../tools/SemVer";
|
|
||||||
import { same } from "evt/tools/inDepth/same";
|
|
||||||
import type { BuildContext } from "./buildContext";
|
|
||||||
import fetch from "make-fetch-happen";
|
|
||||||
|
|
||||||
type GetLatestsSemVersionedTag = ReturnType<
|
|
||||||
typeof getLatestsSemVersionedTagFactory
|
|
||||||
>["getLatestsSemVersionedTag"];
|
|
||||||
|
|
||||||
type Params = Param0<GetLatestsSemVersionedTag>;
|
|
||||||
type R = ReturnType<GetLatestsSemVersionedTag>;
|
|
||||||
|
|
||||||
let getLatestsSemVersionedTag_stateless: GetLatestsSemVersionedTag | undefined =
|
|
||||||
undefined;
|
|
||||||
|
|
||||||
const CACHE_VERSION = 1;
|
|
||||||
|
|
||||||
type Cache = {
|
|
||||||
version: typeof CACHE_VERSION;
|
|
||||||
entries: {
|
|
||||||
time: number;
|
|
||||||
params: Params;
|
|
||||||
result: R;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BuildContextLike = {
|
|
||||||
cacheDirPath: string;
|
|
||||||
fetchOptions: BuildContext["fetchOptions"];
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export async function getLatestsSemVersionedTag({
|
|
||||||
buildContext,
|
|
||||||
...params
|
|
||||||
}: Params & {
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
}): Promise<R> {
|
|
||||||
const cacheFilePath = pathJoin(
|
|
||||||
buildContext.cacheDirPath,
|
|
||||||
"latest-sem-versioned-tags.json"
|
|
||||||
);
|
|
||||||
|
|
||||||
const cacheLookupResult = (() => {
|
|
||||||
const getResult_currentCache = (currentCacheEntries: Cache["entries"]) => ({
|
|
||||||
hasCachedResult: false as const,
|
|
||||||
currentCache: {
|
|
||||||
version: CACHE_VERSION,
|
|
||||||
entries: currentCacheEntries
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fs.existsSync(cacheFilePath)) {
|
|
||||||
return getResult_currentCache([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache_json;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cache_json = fs.readFileSync(cacheFilePath).toString("utf8");
|
|
||||||
} catch {
|
|
||||||
return getResult_currentCache([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache_json_parsed: unknown;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cache_json_parsed = JSON.parse(cache_json);
|
|
||||||
} catch {
|
|
||||||
return getResult_currentCache([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const zSemVer = (() => {
|
|
||||||
type TargetType = SemVer;
|
|
||||||
|
|
||||||
const zTargetType = z.object({
|
|
||||||
major: z.number(),
|
|
||||||
minor: z.number(),
|
|
||||||
patch: z.number(),
|
|
||||||
rc: z.number().optional(),
|
|
||||||
parsedFrom: z.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
|
||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const zCache = (() => {
|
|
||||||
type TargetType = Cache;
|
|
||||||
|
|
||||||
const zTargetType = z.object({
|
|
||||||
version: z.literal(CACHE_VERSION),
|
|
||||||
entries: z.array(
|
|
||||||
z.object({
|
|
||||||
time: z.number(),
|
|
||||||
params: z.object({
|
|
||||||
owner: z.string(),
|
|
||||||
repo: z.string(),
|
|
||||||
count: z.number(),
|
|
||||||
doIgnoreReleaseCandidates: z.boolean()
|
|
||||||
}),
|
|
||||||
result: z.array(
|
|
||||||
z.object({
|
|
||||||
tag: z.string(),
|
|
||||||
version: zSemVer
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
|
||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
|
||||||
})();
|
|
||||||
|
|
||||||
let cache: Cache;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cache = zCache.parse(cache_json_parsed);
|
|
||||||
} catch {
|
|
||||||
return getResult_currentCache([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheEntry = cache.entries.find(e => same(e.params, params));
|
|
||||||
|
|
||||||
if (cacheEntry === undefined) {
|
|
||||||
return getResult_currentCache(cache.entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Date.now() - cacheEntry.time > 3_600_000) {
|
|
||||||
return getResult_currentCache(cache.entries.filter(e => e !== cacheEntry));
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
hasCachedResult: true as const,
|
|
||||||
cachedResult: cacheEntry.result
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (cacheLookupResult.hasCachedResult) {
|
|
||||||
return cacheLookupResult.cachedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentCache } = cacheLookupResult;
|
|
||||||
|
|
||||||
getLatestsSemVersionedTag_stateless ??= (() => {
|
|
||||||
const octokit = (() => {
|
|
||||||
const githubToken = process.env.GITHUB_TOKEN;
|
|
||||||
|
|
||||||
const octokit = new Octokit({
|
|
||||||
...(githubToken === undefined ? {} : { auth: githubToken }),
|
|
||||||
request: {
|
|
||||||
fetch: (url: string, options?: any) =>
|
|
||||||
fetch(url, {
|
|
||||||
...options,
|
|
||||||
...buildContext.fetchOptions
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return octokit;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({
|
|
||||||
octokit
|
|
||||||
});
|
|
||||||
|
|
||||||
return getLatestsSemVersionedTag;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const result = await getLatestsSemVersionedTag_stateless(params);
|
|
||||||
|
|
||||||
currentCache.entries.push({
|
|
||||||
time: Date.now(),
|
|
||||||
params,
|
|
||||||
result
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
const dirPath = pathDirname(cacheFilePath);
|
|
||||||
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(cacheFilePath, JSON.stringify(currentCache, null, 2));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
156
src/bin/shared/initializeSpa.ts
Normal file
156
src/bin/shared/initializeSpa.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import type { BuildContext } from "./buildContext";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { assert, is, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import {
|
||||||
|
addSyncExtensionsToPostinstallScript,
|
||||||
|
type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript
|
||||||
|
} from "./addSyncExtensionsToPostinstallScript";
|
||||||
|
import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
|
||||||
|
import { npmInstall } from "../tools/npmInstall";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { z } from "zod";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export type BuildContextLike = BuildContextLike_addSyncExtensionsToPostinstallScript & {
|
||||||
|
themeSrcDirPath: string;
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function initializeSpa(params: {
|
||||||
|
themeType: "account" | "admin";
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}) {
|
||||||
|
const { themeType, buildContext } = params;
|
||||||
|
|
||||||
|
{
|
||||||
|
const themeTypeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
|
||||||
|
|
||||||
|
if (
|
||||||
|
fs.existsSync(themeTypeSrcDirPath) &&
|
||||||
|
fs.readdirSync(themeTypeSrcDirPath).length > 0
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
chalk.red(
|
||||||
|
`There is already a ${pathRelative(
|
||||||
|
process.cwd(),
|
||||||
|
themeTypeSrcDirPath
|
||||||
|
)} directory in your project. Aborting.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPackageJson = (() => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
scripts?: Record<string, string | undefined>;
|
||||||
|
dependencies?: Record<string, string | undefined>;
|
||||||
|
devDependencies?: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
scripts: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||||
|
dependencies: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||||
|
devDependencies: z.record(z.union([z.string(), z.undefined()])).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson;
|
||||||
|
})();
|
||||||
|
|
||||||
|
addSyncExtensionsToPostinstallScript({
|
||||||
|
parsedPackageJson,
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
const uiSharedMajor = (() => {
|
||||||
|
const dependencies = {
|
||||||
|
...parsedPackageJson.devDependencies,
|
||||||
|
...parsedPackageJson.dependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
const version = dependencies["@keycloakify/keycloak-ui-shared"];
|
||||||
|
|
||||||
|
if (version === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = version.match(/^[^~]?(\d+)\./);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
})();
|
||||||
|
|
||||||
|
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
|
||||||
|
|
||||||
|
const version = ((): string[] => {
|
||||||
|
const cmdOutput = child_process
|
||||||
|
.execSync(`npm show ${moduleName} versions --json`)
|
||||||
|
.toString("utf8")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||||
|
|
||||||
|
// NOTE: Bug in some older npm versions
|
||||||
|
if (typeof versions === "string") {
|
||||||
|
return [versions];
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
})()
|
||||||
|
.reverse()
|
||||||
|
.filter(version => !version.includes("-"))
|
||||||
|
.find(version =>
|
||||||
|
uiSharedMajor === undefined ? true : version.startsWith(`${uiSharedMajor}.`)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(version !== undefined);
|
||||||
|
|
||||||
|
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
|
||||||
|
|
||||||
|
if (parsedPackageJson.devDependencies !== undefined) {
|
||||||
|
delete parsedPackageJson.devDependencies[moduleName];
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
|
||||||
|
|
||||||
|
if (await getIsPrettierAvailable()) {
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
sourceCode,
|
||||||
|
filePath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
buildContext.packageJsonFilePath,
|
||||||
|
Buffer.from(sourceCode, "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await npmInstall({
|
||||||
|
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||||
|
});
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
|
||||||
import type { ThemeType } from "./constants";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
export type MetaInfKeycloakTheme = {
|
|
||||||
themes: { name: string; types: (ThemeType | "email")[] }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function writeMetaInfKeycloakThemes(params: {
|
|
||||||
resourcesDirPath: string;
|
|
||||||
getNewMetaInfKeycloakTheme: (params: {
|
|
||||||
metaInfKeycloakTheme: MetaInfKeycloakTheme | undefined;
|
|
||||||
}) => MetaInfKeycloakTheme;
|
|
||||||
}) {
|
|
||||||
const { resourcesDirPath, getNewMetaInfKeycloakTheme } = params;
|
|
||||||
|
|
||||||
const filePath = pathJoin(resourcesDirPath, "META-INF", "keycloak-themes.json");
|
|
||||||
|
|
||||||
const currentMetaInfKeycloakTheme = !fs.existsSync(filePath)
|
|
||||||
? undefined
|
|
||||||
: (JSON.parse(
|
|
||||||
fs.readFileSync(filePath).toString("utf8")
|
|
||||||
) as MetaInfKeycloakTheme);
|
|
||||||
|
|
||||||
const newMetaInfKeycloakThemes = getNewMetaInfKeycloakTheme({
|
|
||||||
metaInfKeycloakTheme: currentMetaInfKeycloakTheme
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
const dirPath = pathDirname(filePath);
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
filePath,
|
|
||||||
Buffer.from(JSON.stringify(newMetaInfKeycloakThemes, null, 2), "utf8")
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
import {
|
|
||||||
getLatestsSemVersionedTag,
|
|
||||||
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
|
||||||
} from "./getLatestsSemVersionedTag";
|
|
||||||
import cliSelect from "cli-select";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import { SemVer } from "../tools/SemVer";
|
|
||||||
import type { BuildContext } from "./buildContext";
|
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export async function promptKeycloakVersion(params: {
|
|
||||||
startingFromMajor: number | undefined;
|
|
||||||
excludeMajorVersions: number[];
|
|
||||||
doOmitPatch: boolean;
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
}) {
|
|
||||||
const { startingFromMajor, excludeMajorVersions, doOmitPatch, buildContext } = params;
|
|
||||||
|
|
||||||
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
|
|
||||||
|
|
||||||
const semVersionedTags = await getLatestsSemVersionedTag({
|
|
||||||
count: 50,
|
|
||||||
owner: "keycloak",
|
|
||||||
repo: "keycloak",
|
|
||||||
doIgnoreReleaseCandidates: true,
|
|
||||||
buildContext
|
|
||||||
});
|
|
||||||
|
|
||||||
semVersionedTags.forEach(semVersionedTag => {
|
|
||||||
if (
|
|
||||||
startingFromMajor !== undefined &&
|
|
||||||
semVersionedTag.version.major < startingFromMajor
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeMajorVersions.includes(semVersionedTag.version.major)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSemVersionedTag = semVersionedTagByMajor.get(
|
|
||||||
semVersionedTag.version.major
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentSemVersionedTag !== undefined &&
|
|
||||||
SemVer.compare(semVersionedTag.version, currentSemVersionedTag.version) === -1
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
semVersionedTagByMajor.set(semVersionedTag.version.major, semVersionedTag);
|
|
||||||
});
|
|
||||||
|
|
||||||
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(
|
|
||||||
({ version }) =>
|
|
||||||
`${version.major}.${version.minor}${doOmitPatch ? "" : `.${version.patch}`}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const { value } = await cliSelect<string>({
|
|
||||||
values: lastMajorVersions
|
|
||||||
}).catch(() => {
|
|
||||||
process.exit(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const keycloakVersion = value.split(" ")[0];
|
|
||||||
|
|
||||||
return { keycloakVersion };
|
|
||||||
}
|
|
@ -1,18 +1,17 @@
|
|||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { Deferred } from "evt/tools/Deferred";
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert, is, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { sep as pathSep, join as pathJoin } from "path";
|
import { sep as pathSep, join as pathJoin } from "path";
|
||||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { dirname as pathDirname, relative as pathRelative } from "path";
|
import { dirname as pathDirname, relative as pathRelative } from "path";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export type BuildContextLike = {
|
export type BuildContextLike = {
|
||||||
projectDirPath: string;
|
projectDirPath: string;
|
||||||
keycloakifyBuildDirPath: string;
|
|
||||||
bundler: BuildContext["bundler"];
|
|
||||||
projectBuildDirPath: string;
|
|
||||||
packageJsonFilePath: string;
|
packageJsonFilePath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,58 +22,36 @@ export async function appBuild(params: {
|
|||||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
switch (buildContext.bundler) {
|
const { parsedPackageJson } = (() => {
|
||||||
case "vite":
|
type ParsedPackageJson = {
|
||||||
return appBuild_vite({ buildContext });
|
scripts?: Record<string, string>;
|
||||||
case "webpack":
|
};
|
||||||
return appBuild_webpack({ buildContext });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function appBuild_vite(params: {
|
const zParsedPackageJson = (() => {
|
||||||
buildContext: BuildContextLike;
|
type TargetType = ParsedPackageJson;
|
||||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
|
||||||
const { buildContext } = params;
|
|
||||||
|
|
||||||
assert(buildContext.bundler === "vite");
|
const zTargetType = z.object({
|
||||||
|
scripts: z.record(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
const dIsSuccess = new Deferred<boolean>();
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||||
|
|
||||||
console.log(chalk.blue("$ npx vite build"));
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
const child = child_process.spawn("npx", ["vite", "build"], {
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
cwd: buildContext.projectDirPath,
|
|
||||||
shell: true
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stdout.on("data", data => {
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
if (data.toString("utf8").includes("gzip:")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdout.write(data);
|
return { parsedPackageJson };
|
||||||
});
|
})();
|
||||||
|
|
||||||
child.stderr.on("data", data => process.stderr.write(data));
|
const entries = Object.entries(parsedPackageJson.scripts ?? {}).filter(
|
||||||
|
([, scriptCommand]) => scriptCommand.includes("keycloakify build")
|
||||||
child.on("exit", code => dIsSuccess.resolve(code === 0));
|
);
|
||||||
|
|
||||||
const isSuccess = await dIsSuccess.pr;
|
|
||||||
|
|
||||||
return { isAppBuildSuccess: isSuccess };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function appBuild_webpack(params: {
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
|
||||||
const { buildContext } = params;
|
|
||||||
|
|
||||||
assert(buildContext.bundler === "webpack");
|
|
||||||
|
|
||||||
const entries = Object.entries(
|
|
||||||
(JSON.parse(fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8"))
|
|
||||||
.scripts ?? {}) as Record<string, string>
|
|
||||||
).filter(([, scriptCommand]) => scriptCommand.includes("keycloakify build"));
|
|
||||||
|
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
console.log(
|
console.log(
|
||||||
@ -127,6 +104,76 @@ async function appBuild_webpack(params: {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
common_case: {
|
||||||
|
if (appBuildSubCommands.length !== 1) {
|
||||||
|
break common_case;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [appBuildSubCommand] = appBuildSubCommands;
|
||||||
|
|
||||||
|
const isNpmRunBuild = (() => {
|
||||||
|
for (const packageManager of ["npm", "yarn", "pnpm", "bun", "deno"]) {
|
||||||
|
for (const doUseRun of [true, false]) {
|
||||||
|
if (
|
||||||
|
`${packageManager}${doUseRun ? " run " : " "}build` ===
|
||||||
|
appBuildSubCommand
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!isNpmRunBuild) {
|
||||||
|
break common_case;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scripts } = parsedPackageJson;
|
||||||
|
|
||||||
|
assert(scripts !== undefined);
|
||||||
|
|
||||||
|
const buildCmd = scripts.build;
|
||||||
|
|
||||||
|
if (buildCmd !== "tsc && vite build") {
|
||||||
|
break common_case;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scripts.prebuild !== undefined) {
|
||||||
|
break common_case;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scripts.postbuild !== undefined) {
|
||||||
|
break common_case;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dIsSuccess = new Deferred<boolean>();
|
||||||
|
|
||||||
|
console.log(chalk.blue("$ npx vite build"));
|
||||||
|
|
||||||
|
const child = child_process.spawn("npx", ["vite", "build"], {
|
||||||
|
cwd: buildContext.projectDirPath,
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout.on("data", data => {
|
||||||
|
if (data.toString("utf8").includes("gzip:")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", data => process.stderr.write(data));
|
||||||
|
|
||||||
|
child.on("exit", code => dIsSuccess.resolve(code === 0));
|
||||||
|
|
||||||
|
const isSuccess = await dIsSuccess.pr;
|
||||||
|
|
||||||
|
return { isAppBuildSuccess: isSuccess };
|
||||||
|
}
|
||||||
|
|
||||||
let commandCwd = pathDirname(buildContext.packageJsonFilePath);
|
let commandCwd = pathDirname(buildContext.packageJsonFilePath);
|
||||||
|
|
||||||
for (const subCommand of appBuildSubCommands) {
|
for (const subCommand of appBuildSubCommands) {
|
||||||
|
267
src/bin/start-keycloak/getSupportedDockerImageTags.ts
Normal file
267
src/bin/start-keycloak/getSupportedDockerImageTags.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import fetch from "make-fetch-happen";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { SemVer } from "../tools/SemVer";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
|
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
||||||
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
|
import * as fs from "fs/promises";
|
||||||
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||||
|
import type { ReturnType } from "tsafe";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
fetchOptions: BuildContext["fetchOptions"];
|
||||||
|
cacheDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>;
|
||||||
|
|
||||||
|
export async function getSupportedDockerImageTags(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<{
|
||||||
|
allSupportedTags: string[];
|
||||||
|
latestMajorTags: string[];
|
||||||
|
}> {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = await getCachedValue({ cacheDirPath: buildContext.cacheDirPath });
|
||||||
|
|
||||||
|
if (result !== undefined) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags_queryResponse: string[] = [];
|
||||||
|
|
||||||
|
await (async function callee(url: string) {
|
||||||
|
const r = await fetch(url, buildContext.fetchOptions);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
(async () => {
|
||||||
|
tags_queryResponse.push(
|
||||||
|
...z
|
||||||
|
.object({
|
||||||
|
tags: z.array(z.string())
|
||||||
|
})
|
||||||
|
.parse(await r.json()).tags
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
(async () => {
|
||||||
|
const link = r.headers.get("link");
|
||||||
|
|
||||||
|
if (link === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const split = link.split(";").map(s => s.trim());
|
||||||
|
|
||||||
|
assert(split.length === 2);
|
||||||
|
|
||||||
|
assert(split[1] === 'rel="next"');
|
||||||
|
|
||||||
|
const match = split[0].match(/^<(.+)>$/);
|
||||||
|
|
||||||
|
assert(match !== null);
|
||||||
|
|
||||||
|
const nextUrl = new URL(url).origin + match[1];
|
||||||
|
|
||||||
|
await callee(nextUrl);
|
||||||
|
})()
|
||||||
|
]);
|
||||||
|
})("https://quay.io/v2/keycloak/keycloak/tags/list");
|
||||||
|
|
||||||
|
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
|
||||||
|
|
||||||
|
const allSupportedTags_withVersion = tags_queryResponse
|
||||||
|
.map(tag => ({
|
||||||
|
tag,
|
||||||
|
version: (() => {
|
||||||
|
if (tag.includes("-")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let version: SemVer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
version = SemVer.parse(tag);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.split(".").length !== 3) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedKeycloakMajorVersions.includes(version.major)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
})()
|
||||||
|
}))
|
||||||
|
.map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
|
||||||
|
.filter(exclude(undefined))
|
||||||
|
.sort(({ version: a }, { version: b }) => SemVer.compare(b, a));
|
||||||
|
|
||||||
|
const latestTagByMajor: Record<number, SemVer | undefined> = {};
|
||||||
|
|
||||||
|
for (const { version } of allSupportedTags_withVersion) {
|
||||||
|
const version_current = latestTagByMajor[version.major];
|
||||||
|
|
||||||
|
if (
|
||||||
|
version_current === undefined ||
|
||||||
|
SemVer.compare(version_current, version) === -1
|
||||||
|
) {
|
||||||
|
latestTagByMajor[version.major] = version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestMajorTags = Object.entries(latestTagByMajor)
|
||||||
|
.sort(([a], [b]) => parseInt(b) - parseInt(a))
|
||||||
|
.map(([, version]) => version)
|
||||||
|
.map(version => {
|
||||||
|
assert(version !== undefined);
|
||||||
|
|
||||||
|
if (!supportedKeycloakMajorVersions.includes(version.major)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SemVer.stringify(version);
|
||||||
|
})
|
||||||
|
.filter(exclude(undefined));
|
||||||
|
|
||||||
|
const allSupportedTags = allSupportedTags_withVersion.map(({ tag }) => tag);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
latestMajorTags,
|
||||||
|
allSupportedTags
|
||||||
|
};
|
||||||
|
|
||||||
|
await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getCachedValue, setCachedValue } = (() => {
|
||||||
|
type Result = ReturnType<typeof getSupportedDockerImageTags>;
|
||||||
|
|
||||||
|
const zResult = (() => {
|
||||||
|
type TargetType = Result;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
allSupportedTags: z.array(z.string()),
|
||||||
|
latestMajorTags: z.array(z.string())
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<TargetType, InferredType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
type Cache = {
|
||||||
|
keycloakifyVersion: string;
|
||||||
|
time: number;
|
||||||
|
result: Result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zCache = (() => {
|
||||||
|
type TargetType = Cache;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
keycloakifyVersion: z.string(),
|
||||||
|
time: z.number(),
|
||||||
|
result: zResult
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<TargetType, InferredType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
let inMemoryCachedResult: Cache["result"] | undefined = undefined;
|
||||||
|
|
||||||
|
function getCacheFilePath(params: { cacheDirPath: string }) {
|
||||||
|
const { cacheDirPath } = params;
|
||||||
|
|
||||||
|
return pathJoin(cacheDirPath, "supportedDockerImageTags.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCachedValue(params: { cacheDirPath: string }) {
|
||||||
|
const { cacheDirPath } = params;
|
||||||
|
|
||||||
|
if (inMemoryCachedResult !== undefined) {
|
||||||
|
return inMemoryCachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheFilePath = getCacheFilePath({ cacheDirPath });
|
||||||
|
|
||||||
|
if (!(await existsAsync(cacheFilePath))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache: Cache | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
cache = zCache.parse(JSON.parse(await fs.readFile(cacheFilePath, "utf8")));
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache.keycloakifyVersion !== readThisNpmPackageVersion()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - cache.time > 3_600 * 24) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
inMemoryCachedResult = cache.result;
|
||||||
|
|
||||||
|
return cache.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setCachedValue(params: {
|
||||||
|
cacheDirPath: string;
|
||||||
|
result: Cache["result"];
|
||||||
|
}) {
|
||||||
|
const { cacheDirPath, result } = params;
|
||||||
|
|
||||||
|
inMemoryCachedResult = result;
|
||||||
|
|
||||||
|
const cacheFilePath = getCacheFilePath({ cacheDirPath });
|
||||||
|
|
||||||
|
{
|
||||||
|
const dirPath = pathDirname(cacheFilePath);
|
||||||
|
|
||||||
|
if (!(await existsAsync(dirPath))) {
|
||||||
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
cacheFilePath,
|
||||||
|
JSON.stringify(
|
||||||
|
zCache.parse({
|
||||||
|
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||||
|
time: Date.now(),
|
||||||
|
result
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCachedValue,
|
||||||
|
setCachedValue
|
||||||
|
};
|
||||||
|
})();
|
@ -0,0 +1,118 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
|
export type ParsedRealmJson = {
|
||||||
|
realm: string;
|
||||||
|
loginTheme?: string;
|
||||||
|
accountTheme?: string;
|
||||||
|
adminTheme?: string;
|
||||||
|
emailTheme?: string;
|
||||||
|
eventsListeners: string[];
|
||||||
|
users: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
credentials: {
|
||||||
|
type: string /* "password" or something else */;
|
||||||
|
}[];
|
||||||
|
clientRoles?: Record<string, string[]>;
|
||||||
|
}[];
|
||||||
|
roles: {
|
||||||
|
client: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
containerId: string; // client id
|
||||||
|
}[]
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
clients: {
|
||||||
|
id: string;
|
||||||
|
clientId: string; // example: realm-management
|
||||||
|
baseUrl?: string;
|
||||||
|
redirectUris?: string[];
|
||||||
|
webOrigins?: string[];
|
||||||
|
attributes?: {
|
||||||
|
"post.logout.redirect.uris"?: string;
|
||||||
|
};
|
||||||
|
protocol?: string;
|
||||||
|
protocolMappers?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
protocol: string; // "openid-connect" or something else
|
||||||
|
protocolMapper: string; // "oidc-hardcoded-claim-mapper" or something else
|
||||||
|
consentRequired: boolean;
|
||||||
|
config?: Record<string, string>;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const zParsedRealmJson = (() => {
|
||||||
|
type TargetType = ParsedRealmJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
realm: z.string(),
|
||||||
|
loginTheme: z.string().optional(),
|
||||||
|
accountTheme: z.string().optional(),
|
||||||
|
adminTheme: z.string().optional(),
|
||||||
|
emailTheme: z.string().optional(),
|
||||||
|
eventsListeners: z.array(z.string()),
|
||||||
|
users: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
credentials: z.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
clientRoles: z.record(z.array(z.string())).optional()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
roles: z.object({
|
||||||
|
client: z.record(
|
||||||
|
z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
containerId: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
clients: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
clientId: z.string(),
|
||||||
|
baseUrl: z.string().optional(),
|
||||||
|
redirectUris: z.array(z.string()).optional(),
|
||||||
|
webOrigins: z.array(z.string()).optional(),
|
||||||
|
attributes: z
|
||||||
|
.object({
|
||||||
|
"post.logout.redirect.uris": z.string().optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
protocol: z.string().optional(),
|
||||||
|
protocolMappers: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
protocol: z.string(),
|
||||||
|
protocolMapper: z.string(),
|
||||||
|
consentRequired: z.boolean(),
|
||||||
|
config: z.record(z.string()).optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<TargetType, InferredType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
@ -0,0 +1,3 @@
|
|||||||
|
export type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||||
|
export { readRealmJsonFile } from "./readRealmJsonFile";
|
||||||
|
export { writeRealmJsonFile } from "./writeRealmJsonFile";
|
@ -0,0 +1,20 @@
|
|||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { type ParsedRealmJson, zParsedRealmJson } from "./ParsedRealmJson";
|
||||||
|
|
||||||
|
export function readRealmJsonFile(params: {
|
||||||
|
realmJsonFilePath: string;
|
||||||
|
}): ParsedRealmJson {
|
||||||
|
const { realmJsonFilePath } = params;
|
||||||
|
|
||||||
|
const parsedRealmJson = JSON.parse(
|
||||||
|
fs.readFileSync(realmJsonFilePath).toString("utf8")
|
||||||
|
) as unknown;
|
||||||
|
|
||||||
|
zParsedRealmJson.parse(parsedRealmJson);
|
||||||
|
|
||||||
|
assert(is<ParsedRealmJson>(parsedRealmJson));
|
||||||
|
|
||||||
|
return parsedRealmJson;
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { getIsPrettierAvailable, runPrettier } from "../../../tools/runPrettier";
|
||||||
|
import { canonicalStringify } from "../../../tools/canonicalStringify";
|
||||||
|
import type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||||
|
import { getDefaultConfig } from "../defaultConfig";
|
||||||
|
|
||||||
|
export async function writeRealmJsonFile(params: {
|
||||||
|
realmJsonFilePath: string;
|
||||||
|
parsedRealmJson: ParsedRealmJson;
|
||||||
|
keycloakMajorVersionNumber: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { realmJsonFilePath, parsedRealmJson, keycloakMajorVersionNumber } = params;
|
||||||
|
|
||||||
|
let sourceCode = canonicalStringify({
|
||||||
|
data: parsedRealmJson,
|
||||||
|
referenceData: getDefaultConfig({
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (await getIsPrettierAvailable()) {
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
sourceCode: sourceCode,
|
||||||
|
filePath: realmJsonFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.writeFile(realmJsonFilePath, Buffer.from(sourceCode, "utf8"));
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../../../tools/getThisCodebaseRootDirPath";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { readRealmJsonFile } from "../ParsedRealmJson/readRealmJsonFile";
|
||||||
|
import type { ParsedRealmJson } from "../ParsedRealmJson/ParsedRealmJson";
|
||||||
|
|
||||||
|
function getDefaultRealmJsonFilePath(params: { keycloakMajorVersionNumber: number }) {
|
||||||
|
const { keycloakMajorVersionNumber } = params;
|
||||||
|
|
||||||
|
return pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
"start-keycloak",
|
||||||
|
"realmConfig",
|
||||||
|
"defaultConfig",
|
||||||
|
`realm-kc-${keycloakMajorVersionNumber}.json`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { getSupportedKeycloakMajorVersions } = (() => {
|
||||||
|
let cache: number[] | undefined = undefined;
|
||||||
|
|
||||||
|
function getSupportedKeycloakMajorVersions(): number[] {
|
||||||
|
if (cache !== undefined) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache = fs
|
||||||
|
.readdirSync(
|
||||||
|
pathDirname(
|
||||||
|
getDefaultRealmJsonFilePath({ keycloakMajorVersionNumber: 0 })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map(fileBasename => {
|
||||||
|
const match = fileBasename.match(/^realm-kc-(\d+)\.json$/);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = parseInt(match[1]);
|
||||||
|
|
||||||
|
assert(!isNaN(n));
|
||||||
|
|
||||||
|
return n;
|
||||||
|
})
|
||||||
|
.filter(exclude(undefined))
|
||||||
|
.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getSupportedKeycloakMajorVersions };
|
||||||
|
})();
|
||||||
|
|
||||||
|
export function getDefaultConfig(params: {
|
||||||
|
keycloakMajorVersionNumber: number;
|
||||||
|
}): ParsedRealmJson {
|
||||||
|
const { keycloakMajorVersionNumber } = params;
|
||||||
|
|
||||||
|
assert(
|
||||||
|
getSupportedKeycloakMajorVersions().includes(keycloakMajorVersionNumber),
|
||||||
|
`We do not have a default config for Keycloak ${keycloakMajorVersionNumber}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return readRealmJsonFile({
|
||||||
|
realmJsonFilePath: getDefaultRealmJsonFilePath({
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./defaultConfig";
|
@ -73,7 +73,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["delete-account", "view-profile", "manage-account"]
|
"account": ["view-profile", "manage-account", "delete-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -398,6 +398,26 @@
|
|||||||
"otpPolicyLookAheadWindow": 1,
|
"otpPolicyLookAheadWindow": 1,
|
||||||
"otpPolicyPeriod": 30,
|
"otpPolicyPeriod": 30,
|
||||||
"otpSupportedApplications": ["FreeOTP", "Google Authenticator"],
|
"otpSupportedApplications": ["FreeOTP", "Google Authenticator"],
|
||||||
|
"webAuthnPolicyRpEntityName": "keycloak",
|
||||||
|
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||||
|
"webAuthnPolicyRpId": "",
|
||||||
|
"webAuthnPolicyAttestationConveyancePreference": "not specified",
|
||||||
|
"webAuthnPolicyAuthenticatorAttachment": "not specified",
|
||||||
|
"webAuthnPolicyRequireResidentKey": "not specified",
|
||||||
|
"webAuthnPolicyUserVerificationRequirement": "not specified",
|
||||||
|
"webAuthnPolicyCreateTimeout": 0,
|
||||||
|
"webAuthnPolicyAvoidSameAuthenticatorRegister": false,
|
||||||
|
"webAuthnPolicyAcceptableAaguids": [],
|
||||||
|
"webAuthnPolicyPasswordlessRpEntityName": "keycloak",
|
||||||
|
"webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
|
||||||
|
"webAuthnPolicyPasswordlessRpId": "",
|
||||||
|
"webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
|
||||||
|
"webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
|
||||||
|
"webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
|
||||||
|
"webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
|
||||||
|
"webAuthnPolicyPasswordlessCreateTimeout": 0,
|
||||||
|
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
|
||||||
|
"webAuthnPolicyPasswordlessAcceptableAaguids": [],
|
||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"id": "00a62e75-bcc1-419a-a292-63ee5d161ed3",
|
"id": "00a62e75-bcc1-419a-a292-63ee5d161ed3",
|
||||||
@ -422,30 +442,43 @@
|
|||||||
"disableableCredentialTypes": [],
|
"disableableCredentialTypes": [],
|
||||||
"requiredActions": [],
|
"requiredActions": [],
|
||||||
"realmRoles": ["default-roles-myrealm"],
|
"realmRoles": ["default-roles-myrealm"],
|
||||||
|
"clientRoles": {
|
||||||
|
"realm-management": [
|
||||||
|
"create-client",
|
||||||
|
"view-identity-providers",
|
||||||
|
"manage-realm",
|
||||||
|
"query-groups",
|
||||||
|
"manage-clients",
|
||||||
|
"query-users",
|
||||||
|
"realm-admin",
|
||||||
|
"view-authorization",
|
||||||
|
"view-events",
|
||||||
|
"view-clients",
|
||||||
|
"view-realm",
|
||||||
|
"manage-events",
|
||||||
|
"query-realms",
|
||||||
|
"query-clients",
|
||||||
|
"manage-identity-providers",
|
||||||
|
"manage-users",
|
||||||
|
"view-users",
|
||||||
|
"impersonation",
|
||||||
|
"manage-authorization"
|
||||||
|
],
|
||||||
|
"broker": ["read-token"],
|
||||||
|
"account": [
|
||||||
|
"view-profile",
|
||||||
|
"manage-account-links",
|
||||||
|
"view-applications",
|
||||||
|
"manage-consent",
|
||||||
|
"delete-account",
|
||||||
|
"manage-account",
|
||||||
|
"view-consent"
|
||||||
|
]
|
||||||
|
},
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"groups": []
|
"groups": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"webAuthnPolicyRpEntityName": "keycloak",
|
|
||||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
|
||||||
"webAuthnPolicyRpId": "",
|
|
||||||
"webAuthnPolicyAttestationConveyancePreference": "not specified",
|
|
||||||
"webAuthnPolicyAuthenticatorAttachment": "not specified",
|
|
||||||
"webAuthnPolicyRequireResidentKey": "not specified",
|
|
||||||
"webAuthnPolicyUserVerificationRequirement": "not specified",
|
|
||||||
"webAuthnPolicyCreateTimeout": 0,
|
|
||||||
"webAuthnPolicyAvoidSameAuthenticatorRegister": false,
|
|
||||||
"webAuthnPolicyAcceptableAaguids": [],
|
|
||||||
"webAuthnPolicyPasswordlessRpEntityName": "keycloak",
|
|
||||||
"webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
|
|
||||||
"webAuthnPolicyPasswordlessRpId": "",
|
|
||||||
"webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
|
|
||||||
"webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
|
|
||||||
"webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
|
|
||||||
"webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
|
|
||||||
"webAuthnPolicyPasswordlessCreateTimeout": 0,
|
|
||||||
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
|
|
||||||
"webAuthnPolicyPasswordlessAcceptableAaguids": [],
|
|
||||||
"scopeMappings": [
|
"scopeMappings": [
|
||||||
{
|
{
|
||||||
"clientScope": "offline_access",
|
"clientScope": "offline_access",
|
||||||
@ -505,8 +538,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/realms/myrealm/account/*"],
|
"redirectUris": [
|
||||||
"webOrigins": [],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/realms/myrealm/account/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -518,6 +555,7 @@
|
|||||||
"frontchannelLogout": false,
|
"frontchannelLogout": false,
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
|
"post.logout.redirect.uris": "+",
|
||||||
"pkce.code.challenge.method": "S256"
|
"pkce.code.challenge.method": "S256"
|
||||||
},
|
},
|
||||||
"authenticationFlowBindingOverrides": {},
|
"authenticationFlowBindingOverrides": {},
|
||||||
@ -636,7 +674,7 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
"login_theme": "keycloakify-starter",
|
"post.logout.redirect.uris": "+",
|
||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
"backchannel.logout.revoke.offline.tokens": "false"
|
"backchannel.logout.revoke.offline.tokens": "false"
|
||||||
@ -694,8 +732,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/admin/myrealm/console/*"],
|
"redirectUris": [
|
||||||
"webOrigins": ["+"],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/admin/myrealm/console/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -707,12 +749,31 @@
|
|||||||
"frontchannelLogout": false,
|
"frontchannelLogout": false,
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
|
"post.logout.redirect.uris": "+",
|
||||||
"pkce.code.challenge.method": "S256"
|
"pkce.code.challenge.method": "S256"
|
||||||
},
|
},
|
||||||
"authenticationFlowBindingOverrides": {},
|
"authenticationFlowBindingOverrides": {},
|
||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -757,7 +818,8 @@
|
|||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
"config": {
|
"config": {
|
||||||
"id.token.claim": "true",
|
"id.token.claim": "true",
|
||||||
"access.token.claim": "true"
|
"access.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1205,6 +1267,7 @@
|
|||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
"config": {
|
"config": {
|
||||||
"multivalued": "true",
|
"multivalued": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
"user.attribute": "foo",
|
"user.attribute": "foo",
|
||||||
"id.token.claim": "true",
|
"id.token.claim": "true",
|
||||||
"access.token.claim": "true",
|
"access.token.claim": "true",
|
||||||
@ -1271,11 +1334,11 @@
|
|||||||
},
|
},
|
||||||
"smtpServer": {},
|
"smtpServer": {},
|
||||||
"loginTheme": "keycloakify-starter",
|
"loginTheme": "keycloakify-starter",
|
||||||
"accountTheme": "keycloakify-starter",
|
"accountTheme": "",
|
||||||
"adminTheme": "",
|
"adminTheme": "",
|
||||||
"emailTheme": "",
|
"emailTheme": "",
|
||||||
"eventsEnabled": false,
|
"eventsEnabled": false,
|
||||||
"eventsListeners": ["jboss-logging"],
|
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||||
"enabledEventTypes": [],
|
"enabledEventTypes": [],
|
||||||
"adminEventsEnabled": false,
|
"adminEventsEnabled": false,
|
||||||
"adminEventsDetailsEnabled": false,
|
"adminEventsDetailsEnabled": false,
|
||||||
@ -1291,14 +1354,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
|
"oidc-full-name-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"saml-role-list-mapper"
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-usermodel-property-mapper"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1347,14 +1410,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"oidc-address-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
"oidc-full-name-mapper",
|
||||||
"saml-user-property-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-role-list-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"saml-role-list-mapper"
|
"saml-user-property-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1394,6 +1457,12 @@
|
|||||||
"providerId": "rsa-generated",
|
"providerId": "rsa-generated",
|
||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
|
"privateKey": [
|
||||||
|
"MIIEpAIBAAKCAQEA+VQAcuaRivrzLVI8H/tt8PKbtRznTQKmmxOdLRR37leY/ph7sFnEmZt6K02Rvut7R0dxUFtTdiEHUKxhyM8CADMznGUjDYj/EXQzLfZ3LEwbwmR39zp+fZL/H24UDO03zt23Ov9C8Aly0ufXZ1Ic1c33KW6UtUEK/3M52pU8Y0daWdjx7nBj1eRlzWfVG+BYotTTWEnFJuEoZPFQMiXqeA5ob1zZdXjL5JDuGEiBsYjtiiaKbKL5545+FmEBnoCmWXqGu0qWxI2TzvV2dohxfl5KjNzRoKt40ydraiVk5rtBpoNDpeEApuphbokH5dJVwJ5cvWu1CSTnYPW2jXeG4wIDAQABAoIBAQDHV6AcPbhz8/xlafBkabQXBwHzJi7QZaQrLN1n44uX5jWOqP+LmdoULjjZUmWKzd98t+QjKUFrmzCsEYcE9G1XF5jWHA6Qjc3ReKRKxVm28wrmu0knQ39KizKrQGmLhEYwgRg0dU5heExzz6VrGD2xu8E3QRBocp6GauwAlXz4qcnTPHOl8OBPeDHAc0RUdaL5+jRLgKQzf9nnnKB19imBKP++zwrwFrkOZti2ZPs1I7j/ym27mHUbi8TDI2VepDX4QwjjC5a+v3vTsVAGE+1tUAZtqpxpIP9hiUkLH3ajyvp3typhnmZHklqsSZdwtRcK94WiMzL3TkiY70y8abMhAoGBAP8I4EQRXxcKfBn23eaRw8Cd4PFrOouz4zFbYLrBODsvXfku/jnQOMFD0If4IzT6y0FGgBd+t/yqnFJi98oZOKm3P8w+NZBXTbFLH8rgmsElXyS0+9LVMjVa7+UlqZB1eRZbUeLREp03Fsz1y2rflnoWgUnpDIlyhmJqGhCsJdebAoGBAPpFmJ9P42mUTeDWpCyCxgg0zpp6rlpAP8StqZkcvr7kYjhbWrJfJuxrTXtzTTA1zZ59L9EvEAxuug/gl9BkuZ11Uzg8ZLOr4gSuAJZlAORaxJlcoylmNMYIL1fP/K0dxhdO0eHZOpPVpBmGctgev2HBtWp9ZwzQ3DddKimZfNZZAoGAfNOOWSKbhT6HgXnYIHtl8YgUynUuYaR5ZfYQwTfDWwyTFVzP5+IndUjI71Qff1XlWBy2o0lNqmijPJveJlfz6PWdT01/kBd7GnTnqbgHZtPw3pmKzCW3fm/1DRZDCUbGLpAh4z9rufF1wnnnx3aKQ1VykId1sGySo+bEvTZVC1MCgYAlv6uWk/ksKpdYi2d14z+1aymieVClAj3cD4meM4y9xDrgXz8d2mZHkKO+NBT3aZYbCqzUs3GLPoRH8stTPm4UxuaHe+yAgTN1Gz2xcYih6OLwct2VV/oryH5Dk3Z8Mhp314amtxozxCydQP8/g9vABfS0HDgX4cTlgOLkJWeD+QKBgQDuRtsstQ4Q3yK44himPi1JQMMvbYAqyGgRxWH8G1Kr41DV2sQ4wt9CbYxeh6RwMsE+YYNMkTAw1kksUTugWdcDnYpcSVG7xHLJk8WMti0WTqI/7KlkoRehXXv18WJNEXaCr5mJTtJL9wuQcd8nhkEDrrCZubZiJzX9IDnEqZc4Mg=="
|
||||||
|
],
|
||||||
|
"certificate": [
|
||||||
|
"MIICnTCCAYUCBgGTy58etTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE4Mzg0M1oXDTM0MTIxNTE4NDAyM1owEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPlUAHLmkYr68y1SPB/7bfDym7Uc500CppsTnS0Ud+5XmP6Ye7BZxJmbeitNkb7re0dHcVBbU3YhB1CsYcjPAgAzM5xlIw2I/xF0My32dyxMG8Jkd/c6fn2S/x9uFAztN87dtzr/QvAJctLn12dSHNXN9ylulLVBCv9zOdqVPGNHWlnY8e5wY9XkZc1n1RvgWKLU01hJxSbhKGTxUDIl6ngOaG9c2XV4y+SQ7hhIgbGI7Yomimyi+eeOfhZhAZ6Apll6hrtKlsSNk871dnaIcX5eSozc0aCreNMna2olZOa7QaaDQ6XhAKbqYW6JB+XSVcCeXL1rtQkk52D1to13huMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAH/nsEi88hFiNPCWYvTB3lERZpeUCbpDzAXQT/4TONmOw8zi7Cd2OlX8BGBFqjh/fESHv+adlzsY1mUdMvpVaYgHr3gYi8sBSrq5TMUfSYaWp4WCD7utiXXGprG08GCdbye1lpyyNnniWp12Bgjao+rtGamL/M1d6+WZTC+XL+H30u4VHURAiFBsAEoX6tlGV8ynhYOr/b8B43jy0/R0JfrzLjwSKEcA6RfKM7ozbZ0QZuQDALULymPIesrV4mvZ2Qwg4YgpAKaki9Sse45yiIhsIY0p5RnuNZRZnCbukyeBzIyDJobEBGhpui/KT2dqXBlRgRuOhCUf7OGCcPVHKNQ=="
|
||||||
|
],
|
||||||
"priority": ["100"]
|
"priority": ["100"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1403,6 +1472,12 @@
|
|||||||
"providerId": "rsa-enc-generated",
|
"providerId": "rsa-enc-generated",
|
||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
|
"privateKey": [
|
||||||
|
"MIIEogIBAAKCAQEAn82AU+InXwYlE8u9lMwhQghZB7oQ71Hg3PdFqS9ICGzw1u1JcENooCsZse55V6nqptdYF1oZA8QrxnhHzCVCGIqFHtXSoPGHVtozO3Fe1cVIVFm1D9TNS3JHe1C8SBQQT4hGItO5cjDyfGdK3x09RkoAcelrzH5uQ78zd0FKHkzbsTMsP2V8V94c35+ViIUjyGhH2T2BpIyGRLignL+6d0wHbw463L1Ewj/J9z8BtNLCH9PaVLWiGQARjlWyL9vtWBig9XXL0Z9tZUuoLihjh4StkXt2lQ++DKxUklsAjyenRAG5d72T2rY8MO5a1Z2ZSt8+s86D5esrAEIFZc9mqwIDAQABAoIBAAmmCcqGzCPDpjd0xMSYMqXfBSkfReh9RBtzXqRhc3L2yO/hMd7yYv3QvGNu56qwWreqJup6CSqeDJqWJpef5EbBDlqXRHltO+O1lwROyxATMlPNes4y5hZZFxHOBSBA/d8fdkSiDf9kDzANuIqSJGH7E93M3zJgq92xTLU1nvkHR/VYJQv+j+Pjye7MWvjIePfhwFeBqEWlWPTlw/080Mpfp8Hhbl6JeKjx2inkSphp43v4wR1Wmp+E2JIHF4P4sVXPPuPf3JDwg5uGOrROw1ziloD3jTI+LnQ+kRm6R2EbqRqqVsehXT7mZy2puQNqVc4vVqWQdxIErMBazYEpZOECgYEA+8PEcDiIPr2PTYZk+/jErRVYwsxyLgDJexPak7onLxLBJRNRnp1Uk6b1LXM6af5qp+Y510kyAe1k+9xkQLx1gW8rMka9rvVsM+1A2ACvF99V23sRw29CVxeFV/zNn83MinYPX5biUl6MkOX2PvWUhdwRGhKByjiYcAeBOsXkz3ECgYEAon2yYXGzph8Vb8Fetv0wFFbjQOixuL02OjVp/nU1XVE8Aw9BJ7uzA6GQ7akPG0HsaUq7AEHP1uUOsJWQTNQ8WYD9LDuDOl/JFqkG+zrmdUdm0mAIYyH1/GBqgaTLvMq78qqosua8BBJojEyoXDz69UBHpu7cwtUgmzRNQSYqgdsCgYASvD3JEBvrd1XLsh2ftqKEMtt5G5e/nqVfuFmCts6lrSKcbLSdNh4OItWJ/VIygxFSz0osoDDNfeoO6Ba5zox8BlbTlfoVpAPaVWSG7n4ZK7CK9bybq5LnQkPVCWYP51O6VhDMz0CmWozhV4ucoc/cqkTHiOsJrm6Bn71ZL1LYsQKBgFNb8qgk4YnGhoPHiuSLbR/yFzGUbqAciXZBMrg0vwS5iPT03XMZytOBDk2uHi7YmgTGLrsKCCrxZaDXiaiwdKliD/+iJEdNHmc+nXNDGzltQOWKGKNqp7wqZllOBqs6wkLSpCrrTec03mejZ/ex3Pj2WgvcnGpjVg/pO/zBLKtjAoGACzGQNEF93fabHQJTsHmb/g+jO2iumjF6ZIWzdFh2KzQABONcoBvy1MJNASFQj3iVy/8kEo4SfmexvMWLBW9igi2z1pHeHY32EuImzuc4xnVDm6dkmDdsO43Ex6CFBx8lM40H4l27mXu+EZRzGClUY8TnmV/FBGmX+LPtOiiwT7s="
|
||||||
|
],
|
||||||
|
"certificate": [
|
||||||
|
"MIICnTCCAYUCBgGTy58fHjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE4Mzg0M1oXDTM0MTIxNTE4NDAyM1owEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/NgFPiJ18GJRPLvZTMIUIIWQe6EO9R4Nz3RakvSAhs8NbtSXBDaKArGbHueVep6qbXWBdaGQPEK8Z4R8wlQhiKhR7V0qDxh1baMztxXtXFSFRZtQ/UzUtyR3tQvEgUEE+IRiLTuXIw8nxnSt8dPUZKAHHpa8x+bkO/M3dBSh5M27EzLD9lfFfeHN+flYiFI8hoR9k9gaSMhkS4oJy/undMB28OOty9RMI/yfc/AbTSwh/T2lS1ohkAEY5Vsi/b7VgYoPV1y9GfbWVLqC4oY4eErZF7dpUPvgysVJJbAI8np0QBuXe9k9q2PDDuWtWdmUrfPrPOg+XrKwBCBWXPZqsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEATwmKBzLiZiUjyB9BWUR4BCXh46DxsiM0BCublewlUFY6FBTn7ea6q3G+X3QP2WM6xa0oAmQz9dq1KChbIoC2WPbceAbwd5XZZfziWsRCv6+xPswtpHPIrsenz8TR4K4P73aeCC+vTVs/y+2tGPEVbnSkcNnOP71hRQGlt0LvjKlEetJSRyYz5depSdJOjl4F3ehpxQtTK/48xUVAytu9ZotJj6AUA7jWFlP5GHgoB+mPk6QTHNWddnc7BQx2FMvg151vxu722ywLh5Dh7WzgFhJNwkX4xpwzhfo0Q1gSygGTdZaJCGj5jfF+KwdiKpN04UxJ8OrRgJqklQgrSVnsgQ=="
|
||||||
|
],
|
||||||
"priority": ["100"],
|
"priority": ["100"],
|
||||||
"algorithm": ["RSA-OAEP"]
|
"algorithm": ["RSA-OAEP"]
|
||||||
}
|
}
|
||||||
@ -1413,6 +1488,8 @@
|
|||||||
"providerId": "aes-generated",
|
"providerId": "aes-generated",
|
||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
|
"kid": ["132fb843-59e9-4f36-ad55-5ce2d3a13fb3"],
|
||||||
|
"secret": ["ETyyqapnrkUsNXLQ-tBVKw"],
|
||||||
"priority": ["100"]
|
"priority": ["100"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1422,6 +1499,10 @@
|
|||||||
"providerId": "hmac-generated",
|
"providerId": "hmac-generated",
|
||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
|
"kid": ["5110d380-c930-49d9-b91b-87f338f6170b"],
|
||||||
|
"secret": [
|
||||||
|
"uCpQrJvP5OBuTxXfDb4JRL0bCKpXUgfGn5vb8UvL-Sfs_sZ9rtvBmd6vuFWARqyezjJQtpoNlMv7sXgxkN-yxQ"
|
||||||
|
],
|
||||||
"priority": ["100"],
|
"priority": ["100"],
|
||||||
"algorithm": ["HS256"]
|
"algorithm": ["HS256"]
|
||||||
}
|
}
|
||||||
@ -1454,7 +1535,7 @@
|
|||||||
"defaultLocale": "en",
|
"defaultLocale": "en",
|
||||||
"authenticationFlows": [
|
"authenticationFlows": [
|
||||||
{
|
{
|
||||||
"id": "f7f2b89b-43cb-491d-8e7c-f1814024a6da",
|
"id": "f664efe4-102d-4ec1-bf11-11af67e3f178",
|
||||||
"alias": "Account verification options",
|
"alias": "Account verification options",
|
||||||
"description": "Method with which to verity the existing account",
|
"description": "Method with which to verity the existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1480,7 +1561,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "17cdac6f-d2a3-4907-8d44-a42827610b63",
|
"id": "8a5630c5-eca1-4b6a-8e59-459cb6c84535",
|
||||||
"alias": "Authentication Options",
|
"alias": "Authentication Options",
|
||||||
"description": "Authentication options.",
|
"description": "Authentication options.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1514,7 +1595,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "53a3e43f-9468-401f-8051-40f982d12f85",
|
"id": "c1a3eed3-25ce-44ae-93d1-f0b8148a0f8c",
|
||||||
"alias": "Browser - Conditional OTP",
|
"alias": "Browser - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1540,7 +1621,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "26286808-3b7b-43df-b32e-af55a37af2e9",
|
"id": "6eb188ad-1041-44dd-bf8f-37cae0d98bf1",
|
||||||
"alias": "Direct Grant - Conditional OTP",
|
"alias": "Direct Grant - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1566,7 +1647,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "8a6a752a-9a9a-4d38-b1f8-edf0a9433490",
|
"id": "4ee215ac-f4e5-4edb-bf76-65dc9e211543",
|
||||||
"alias": "First broker login - Conditional OTP",
|
"alias": "First broker login - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1592,7 +1673,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a6f6804c-4160-4a84-8a1f-c2747a2d3f27",
|
"id": "5a1eac7e-06a0-46d8-b9ae-1f2c934331f9",
|
||||||
"alias": "Handle Existing Account",
|
"alias": "Handle Existing Account",
|
||||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1618,7 +1699,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "740baa9e-8328-4035-9e1a-8fc1616d1f0f",
|
"id": "ed165166-4521-4a62-b185-c4b51643cbb1",
|
||||||
"alias": "Reset - Conditional OTP",
|
"alias": "Reset - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1644,7 +1725,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e60187a8-3e16-4a0c-9daa-f3a4a1fcfdba",
|
"id": "4788fb1f-fd81-4f5d-9abe-4199dd641c1e",
|
||||||
"alias": "User creation or linking",
|
"alias": "User creation or linking",
|
||||||
"description": "Flow for the existing/non-existing user alternatives",
|
"description": "Flow for the existing/non-existing user alternatives",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1671,7 +1752,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "d959d0c2-4004-4633-b280-f80d6423f574",
|
"id": "d778a70f-f472-4dd3-ac40-cb5612ddc171",
|
||||||
"alias": "Verify Existing Account by Re-authentication",
|
"alias": "Verify Existing Account by Re-authentication",
|
||||||
"description": "Reauthentication of existing account",
|
"description": "Reauthentication of existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1697,7 +1778,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ba02689d-b9e8-4a4b-8fdd-0d1386b198fc",
|
"id": "9c1ea8ea-7c23-4e60-b02d-1900d9dc4109",
|
||||||
"alias": "browser",
|
"alias": "browser",
|
||||||
"description": "browser based authentication",
|
"description": "browser based authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1739,7 +1820,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "f09ac92a-e091-4e84-9cd1-cb905ca57b89",
|
"id": "0ebdf418-d57d-4318-9359-7bd0cb2381f2",
|
||||||
"alias": "clients",
|
"alias": "clients",
|
||||||
"description": "Base authentication for clients",
|
"description": "Base authentication for clients",
|
||||||
"providerId": "client-flow",
|
"providerId": "client-flow",
|
||||||
@ -1781,7 +1862,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "aaf72b22-cec4-4714-93d6-f54d5a986ab8",
|
"id": "5cc89293-c72e-4c5e-b31c-15558588a60d",
|
||||||
"alias": "direct grant",
|
"alias": "direct grant",
|
||||||
"description": "OpenID Connect Resource Owner Grant",
|
"description": "OpenID Connect Resource Owner Grant",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1815,7 +1896,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c4a54bb3-f009-4231-a82b-376c2515e07e",
|
"id": "5ae5a321-ccac-449e-9c19-d6dc22ab8085",
|
||||||
"alias": "docker auth",
|
"alias": "docker auth",
|
||||||
"description": "Used by Docker clients to authenticate against the IDP",
|
"description": "Used by Docker clients to authenticate against the IDP",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1833,7 +1914,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "f55ded54-683a-4f5a-a101-9cfbd7b96781",
|
"id": "7737fdd1-0875-47e6-977b-12561cddfdc3",
|
||||||
"alias": "first broker login",
|
"alias": "first broker login",
|
||||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1860,7 +1941,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "931d5a82-378f-4533-8c69-2239a4acd047",
|
"id": "90f975c3-9826-461f-88ca-27c697aff86b",
|
||||||
"alias": "forms",
|
"alias": "forms",
|
||||||
"description": "Username, password, otp and other auth forms.",
|
"description": "Username, password, otp and other auth forms.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1886,7 +1967,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "22b05374-f480-4ca8-aca8-9db8b6dd1729",
|
"id": "ce2722d5-9f4f-41a2-8f81-e01f7b6cee57",
|
||||||
"alias": "http challenge",
|
"alias": "http challenge",
|
||||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1912,7 +1993,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c0371832-e4b7-485e-bf23-6babe4c6ac83",
|
"id": "31b5bfa7-98ad-47a2-b8e6-0669022cd8cb",
|
||||||
"alias": "registration",
|
"alias": "registration",
|
||||||
"description": "registration flow",
|
"description": "registration flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1931,7 +2012,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "4d0445da-073e-465e-b25b-af522915c73f",
|
"id": "bf8a950b-be3b-4e44-8602-64e0bba492eb",
|
||||||
"alias": "registration form",
|
"alias": "registration form",
|
||||||
"description": "registration form",
|
"description": "registration form",
|
||||||
"providerId": "form-flow",
|
"providerId": "form-flow",
|
||||||
@ -1973,7 +2054,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "740d467f-4203-425b-8203-9bfd3eed25ae",
|
"id": "e3519800-971b-4b1d-b64e-3983ccd02dea",
|
||||||
"alias": "reset credentials",
|
"alias": "reset credentials",
|
||||||
"description": "Reset credentials for a user if they forgot their password or something",
|
"description": "Reset credentials for a user if they forgot their password or something",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2015,7 +2096,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cf1a9af9-dadd-4cb9-a26e-fbbba216f8e1",
|
"id": "9d5a33a2-e777-4beb-95de-b84812f69c56",
|
||||||
"alias": "saml ecp",
|
"alias": "saml ecp",
|
||||||
"description": "SAML ECP Profile Authentication Flow",
|
"description": "SAML ECP Profile Authentication Flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2035,14 +2116,14 @@
|
|||||||
],
|
],
|
||||||
"authenticatorConfig": [
|
"authenticatorConfig": [
|
||||||
{
|
{
|
||||||
"id": "4e65eb4b-9f0a-4ab8-98b2-6daf50cd1bf8",
|
"id": "4901c91d-59bd-4727-b585-8e4e44828d0a",
|
||||||
"alias": "create unique user config",
|
"alias": "create unique user config",
|
||||||
"config": {
|
"config": {
|
||||||
"require.password.update.after.registration": "false"
|
"require.password.update.after.registration": "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5e8dc1c5-1489-4d39-bb75-9c499583b91b",
|
"id": "5062a078-83a7-4933-b0d5-3f75cc2a5003",
|
||||||
"alias": "review profile config",
|
"alias": "review profile config",
|
||||||
"config": {
|
"config": {
|
||||||
"update.profile.on.first.login": "missing"
|
"update.profile.on.first.login": "missing"
|
||||||
@ -2132,8 +2213,8 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"cibaBackchannelTokenDeliveryMode": "poll",
|
"cibaBackchannelTokenDeliveryMode": "poll",
|
||||||
"cibaAuthRequestedUserHint": "login_hint",
|
"cibaAuthRequestedUserHint": "login_hint",
|
||||||
"oauth2DevicePollingInterval": "5",
|
|
||||||
"clientOfflineSessionMaxLifespan": "0",
|
"clientOfflineSessionMaxLifespan": "0",
|
||||||
|
"oauth2DevicePollingInterval": "5",
|
||||||
"clientSessionIdleTimeout": "0",
|
"clientSessionIdleTimeout": "0",
|
||||||
"userProfileEnabled": "true",
|
"userProfileEnabled": "true",
|
||||||
"clientOfflineSessionIdleTimeout": "0",
|
"clientOfflineSessionIdleTimeout": "0",
|
@ -73,7 +73,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["delete-account", "view-profile", "manage-account"]
|
"account": ["view-profile", "manage-account", "delete-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -435,13 +435,46 @@
|
|||||||
"type": "password",
|
"type": "password",
|
||||||
"userLabel": "My password",
|
"userLabel": "My password",
|
||||||
"createdDate": 1716214710762,
|
"createdDate": 1716214710762,
|
||||||
"secretData": "{\"value\":\"OaI4sKqQn+NZtS6N/bcqoZ8Q+ucpBby1n4XmzVmioKw=\",\"salt\":\"temixVCSbpA7Genml2KTAw==\",\"additionalParameters\":{}}",
|
"secretData": "{\"value\":\"QzJjOdXU0L9Pdxdx1V5xUs7BY9beGlmN8NpR2qiWxbkjrQ434Q1GwSiJKekZQ/zrLDtNZ7sAbVu+SS+XIe9Zaw==\",\"salt\":\"x8cABpa0Hk/nJ2BPKdFXTg==\",\"additionalParameters\":{}}",
|
||||||
"credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
|
"credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"disableableCredentialTypes": [],
|
"disableableCredentialTypes": [],
|
||||||
"requiredActions": [],
|
"requiredActions": [],
|
||||||
"realmRoles": ["default-roles-myrealm"],
|
"realmRoles": ["default-roles-myrealm"],
|
||||||
|
"clientRoles": {
|
||||||
|
"realm-management": [
|
||||||
|
"create-client",
|
||||||
|
"view-identity-providers",
|
||||||
|
"manage-realm",
|
||||||
|
"query-groups",
|
||||||
|
"manage-clients",
|
||||||
|
"query-users",
|
||||||
|
"realm-admin",
|
||||||
|
"view-authorization",
|
||||||
|
"view-events",
|
||||||
|
"view-clients",
|
||||||
|
"view-realm",
|
||||||
|
"manage-events",
|
||||||
|
"query-realms",
|
||||||
|
"query-clients",
|
||||||
|
"manage-identity-providers",
|
||||||
|
"manage-users",
|
||||||
|
"view-users",
|
||||||
|
"impersonation",
|
||||||
|
"manage-authorization"
|
||||||
|
],
|
||||||
|
"broker": ["read-token"],
|
||||||
|
"account": [
|
||||||
|
"view-profile",
|
||||||
|
"manage-account-links",
|
||||||
|
"view-applications",
|
||||||
|
"manage-consent",
|
||||||
|
"delete-account",
|
||||||
|
"manage-account",
|
||||||
|
"view-consent"
|
||||||
|
]
|
||||||
|
},
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"groups": []
|
"groups": []
|
||||||
}
|
}
|
||||||
@ -507,8 +540,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/realms/myrealm/account/*"],
|
"redirectUris": [
|
||||||
"webOrigins": [],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/realms/myrealm/account/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -643,7 +680,6 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
"login_theme": "keycloakify-starter",
|
|
||||||
"post.logout.redirect.uris": "+",
|
"post.logout.redirect.uris": "+",
|
||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
@ -704,8 +740,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/admin/myrealm/console/*"],
|
"redirectUris": [
|
||||||
"webOrigins": ["+"],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/admin/myrealm/console/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -724,6 +764,24 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -1284,11 +1342,11 @@
|
|||||||
},
|
},
|
||||||
"smtpServer": {},
|
"smtpServer": {},
|
||||||
"loginTheme": "keycloakify-starter",
|
"loginTheme": "keycloakify-starter",
|
||||||
"accountTheme": "keycloakify-starter",
|
"accountTheme": "",
|
||||||
"adminTheme": "",
|
"adminTheme": "",
|
||||||
"emailTheme": "",
|
"emailTheme": "",
|
||||||
"eventsEnabled": false,
|
"eventsEnabled": false,
|
||||||
"eventsListeners": ["jboss-logging"],
|
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||||
"enabledEventTypes": [],
|
"enabledEventTypes": [],
|
||||||
"adminEventsEnabled": false,
|
"adminEventsEnabled": false,
|
||||||
"adminEventsDetailsEnabled": false,
|
"adminEventsDetailsEnabled": false,
|
||||||
@ -1304,14 +1362,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
|
"saml-user-attribute-mapper",
|
||||||
"oidc-full-name-mapper",
|
"oidc-full-name-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-role-list-mapper",
|
||||||
"saml-role-list-mapper"
|
"oidc-usermodel-property-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1361,13 +1419,13 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
"saml-user-attribute-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"oidc-full-name-mapper",
|
||||||
|
"saml-user-attribute-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper"
|
||||||
"oidc-usermodel-property-mapper"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1485,7 +1543,7 @@
|
|||||||
"defaultLocale": "en",
|
"defaultLocale": "en",
|
||||||
"authenticationFlows": [
|
"authenticationFlows": [
|
||||||
{
|
{
|
||||||
"id": "e134634e-f219-4df4-867c-8110688d8e56",
|
"id": "8ccfe057-5ce6-499b-9fae-3cd89b62bf01",
|
||||||
"alias": "Account verification options",
|
"alias": "Account verification options",
|
||||||
"description": "Method with which to verity the existing account",
|
"description": "Method with which to verity the existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1511,7 +1569,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a611a8eb-9626-4aa4-8b54-ee565ea6e5dc",
|
"id": "f3b9ab2e-41c2-4e73-876b-e2c275d6d14e",
|
||||||
"alias": "Authentication Options",
|
"alias": "Authentication Options",
|
||||||
"description": "Authentication options.",
|
"description": "Authentication options.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1545,7 +1603,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "d87cbb31-5c69-45c8-888d-f9649ebbbf97",
|
"id": "df1329cc-777c-42d8-aa2f-c5d5ddaaf5a4",
|
||||||
"alias": "Browser - Conditional OTP",
|
"alias": "Browser - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1571,7 +1629,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "752ba282-a369-4592-92e8-b4287192dbbf",
|
"id": "f78a4cbc-66ff-4caa-8066-67aff94946f4",
|
||||||
"alias": "Direct Grant - Conditional OTP",
|
"alias": "Direct Grant - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1597,7 +1655,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2349282e-40ff-431a-984d-53911511e3d3",
|
"id": "4b20995b-5553-45db-86b0-05c3fe14edb1",
|
||||||
"alias": "First broker login - Conditional OTP",
|
"alias": "First broker login - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1623,7 +1681,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "4ff5463d-26d9-4219-ba85-41464401098f",
|
"id": "0a7cc6b7-e427-4f72-b44e-a02133241bad",
|
||||||
"alias": "Handle Existing Account",
|
"alias": "Handle Existing Account",
|
||||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1649,7 +1707,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "87bb6c6d-cca8-4832-b5ab-67ecb9454a42",
|
"id": "e24e73c0-dd51-4fdc-a916-284f11f38487",
|
||||||
"alias": "Reset - Conditional OTP",
|
"alias": "Reset - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1675,7 +1733,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1fc3d028-0e0a-43a4-aaf9-ba7f7d60b409",
|
"id": "37ee5a12-01c2-41b0-aafa-e9c6661ff544",
|
||||||
"alias": "User creation or linking",
|
"alias": "User creation or linking",
|
||||||
"description": "Flow for the existing/non-existing user alternatives",
|
"description": "Flow for the existing/non-existing user alternatives",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1702,7 +1760,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "036aae59-641f-4799-9124-c7e5034af6c1",
|
"id": "8902a1a7-c2ee-4648-869f-dd5ef89184fc",
|
||||||
"alias": "Verify Existing Account by Re-authentication",
|
"alias": "Verify Existing Account by Re-authentication",
|
||||||
"description": "Reauthentication of existing account",
|
"description": "Reauthentication of existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1728,7 +1786,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2e8b9f28-93b8-4368-84b0-1a8326daafe0",
|
"id": "77c78eed-4bcd-4779-b39f-10135be84946",
|
||||||
"alias": "browser",
|
"alias": "browser",
|
||||||
"description": "browser based authentication",
|
"description": "browser based authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1770,7 +1828,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "0b826105-8493-45ce-87b3-7d917d190b39",
|
"id": "c6398883-01e6-47a1-bb97-c09f2983155d",
|
||||||
"alias": "clients",
|
"alias": "clients",
|
||||||
"description": "Base authentication for clients",
|
"description": "Base authentication for clients",
|
||||||
"providerId": "client-flow",
|
"providerId": "client-flow",
|
||||||
@ -1812,7 +1870,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bf6d9edd-48d8-4392-bbc8-4b17a6866074",
|
"id": "78ab5fb8-f35b-4053-b264-94b208000b13",
|
||||||
"alias": "direct grant",
|
"alias": "direct grant",
|
||||||
"description": "OpenID Connect Resource Owner Grant",
|
"description": "OpenID Connect Resource Owner Grant",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1846,7 +1904,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "97e31722-dd11-42be-aa99-88788fa2dde6",
|
"id": "959e154b-034e-413d-9b19-211e7d9ba33d",
|
||||||
"alias": "docker auth",
|
"alias": "docker auth",
|
||||||
"description": "Used by Docker clients to authenticate against the IDP",
|
"description": "Used by Docker clients to authenticate against the IDP",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1864,7 +1922,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "3f45cf34-231f-4ea1-8e58-d636c451a76b",
|
"id": "001e253d-bdbd-41e2-81c7-1c7b239feeb1",
|
||||||
"alias": "first broker login",
|
"alias": "first broker login",
|
||||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1891,7 +1949,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "9bef2f7c-f989-4871-aaa7-18e2cfa73f22",
|
"id": "45481bb0-18fe-4a26-a77c-35a5afe58436",
|
||||||
"alias": "forms",
|
"alias": "forms",
|
||||||
"description": "Username, password, otp and other auth forms.",
|
"description": "Username, password, otp and other auth forms.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1917,7 +1975,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "0bfaa325-acde-4443-8bd8-1dc2ae759c5f",
|
"id": "bb47b847-5a55-4c08-909e-9f6f8d8a0636",
|
||||||
"alias": "http challenge",
|
"alias": "http challenge",
|
||||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1943,7 +2001,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "37ddbe8c-abf3-4654-bd6d-ffabbeefbb98",
|
"id": "77e6e169-05b7-4b89-af00-09cfe1604eed",
|
||||||
"alias": "registration",
|
"alias": "registration",
|
||||||
"description": "registration flow",
|
"description": "registration flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1962,7 +2020,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5d7b4bc9-e93b-40da-aeb6-ba0c38392f1a",
|
"id": "aef03fe8-1a70-40c3-879f-25588f75c119",
|
||||||
"alias": "registration form",
|
"alias": "registration form",
|
||||||
"description": "registration form",
|
"description": "registration form",
|
||||||
"providerId": "form-flow",
|
"providerId": "form-flow",
|
||||||
@ -2004,7 +2062,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ee7a56e4-c827-4f24-8b8b-8476050b0b64",
|
"id": "990abff7-e2ba-4217-984e-8890cbc2b3a9",
|
||||||
"alias": "reset credentials",
|
"alias": "reset credentials",
|
||||||
"description": "Reset credentials for a user if they forgot their password or something",
|
"description": "Reset credentials for a user if they forgot their password or something",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2046,7 +2104,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "360f0031-4c3b-4272-84ca-2172d430b4bc",
|
"id": "d9894cf6-2f99-493e-ac47-853f54bfc9c6",
|
||||||
"alias": "saml ecp",
|
"alias": "saml ecp",
|
||||||
"description": "SAML ECP Profile Authentication Flow",
|
"description": "SAML ECP Profile Authentication Flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2066,14 +2124,14 @@
|
|||||||
],
|
],
|
||||||
"authenticatorConfig": [
|
"authenticatorConfig": [
|
||||||
{
|
{
|
||||||
"id": "53630acd-a33a-40e3-8786-cf85464c6f9e",
|
"id": "101ed8ff-4383-4539-aa52-2d1e69698b78",
|
||||||
"alias": "create unique user config",
|
"alias": "create unique user config",
|
||||||
"config": {
|
"config": {
|
||||||
"require.password.update.after.registration": "false"
|
"require.password.update.after.registration": "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c0d2b6a0-caad-4e90-b040-17cacdaf70bb",
|
"id": "049042a5-3551-4c16-81a1-64d86f5aa1e5",
|
||||||
"alias": "review profile config",
|
"alias": "review profile config",
|
||||||
"config": {
|
"config": {
|
||||||
"update.profile.on.first.login": "missing"
|
"update.profile.on.first.login": "missing"
|
@ -73,7 +73,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["delete-account", "view-profile", "manage-account"]
|
"account": ["view-profile", "manage-account", "delete-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -407,7 +407,7 @@
|
|||||||
"otpPolicyLookAheadWindow": 1,
|
"otpPolicyLookAheadWindow": 1,
|
||||||
"otpPolicyPeriod": 30,
|
"otpPolicyPeriod": 30,
|
||||||
"otpPolicyCodeReusable": false,
|
"otpPolicyCodeReusable": false,
|
||||||
"otpSupportedApplications": ["totpAppGoogleName", "totpAppFreeOTPName"],
|
"otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName"],
|
||||||
"webAuthnPolicyRpEntityName": "keycloak",
|
"webAuthnPolicyRpEntityName": "keycloak",
|
||||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||||
"webAuthnPolicyRpId": "",
|
"webAuthnPolicyRpId": "",
|
||||||
@ -452,6 +452,40 @@
|
|||||||
"disableableCredentialTypes": [],
|
"disableableCredentialTypes": [],
|
||||||
"requiredActions": [],
|
"requiredActions": [],
|
||||||
"realmRoles": ["default-roles-myrealm"],
|
"realmRoles": ["default-roles-myrealm"],
|
||||||
|
"clientRoles": {
|
||||||
|
"realm-management": [
|
||||||
|
"create-client",
|
||||||
|
"view-identity-providers",
|
||||||
|
"manage-realm",
|
||||||
|
"query-groups",
|
||||||
|
"manage-clients",
|
||||||
|
"query-users",
|
||||||
|
"realm-admin",
|
||||||
|
"view-authorization",
|
||||||
|
"view-events",
|
||||||
|
"view-clients",
|
||||||
|
"view-realm",
|
||||||
|
"manage-events",
|
||||||
|
"query-realms",
|
||||||
|
"query-clients",
|
||||||
|
"manage-identity-providers",
|
||||||
|
"manage-users",
|
||||||
|
"view-users",
|
||||||
|
"impersonation",
|
||||||
|
"manage-authorization"
|
||||||
|
],
|
||||||
|
"broker": ["read-token"],
|
||||||
|
"account": [
|
||||||
|
"view-profile",
|
||||||
|
"manage-account-links",
|
||||||
|
"view-applications",
|
||||||
|
"manage-consent",
|
||||||
|
"delete-account",
|
||||||
|
"manage-account",
|
||||||
|
"view-groups",
|
||||||
|
"view-consent"
|
||||||
|
]
|
||||||
|
},
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"groups": []
|
"groups": []
|
||||||
}
|
}
|
||||||
@ -517,8 +551,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/realms/myrealm/account/*"],
|
"redirectUris": [
|
||||||
"webOrigins": [],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/realms/myrealm/account/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -653,7 +691,6 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
"login_theme": "keycloakify-starter",
|
|
||||||
"post.logout.redirect.uris": "+",
|
"post.logout.redirect.uris": "+",
|
||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
@ -714,8 +751,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/admin/myrealm/console/*"],
|
"redirectUris": [
|
||||||
"webOrigins": ["+"],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/admin/myrealm/console/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -734,6 +775,24 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -1294,11 +1353,11 @@
|
|||||||
},
|
},
|
||||||
"smtpServer": {},
|
"smtpServer": {},
|
||||||
"loginTheme": "keycloakify-starter",
|
"loginTheme": "keycloakify-starter",
|
||||||
"accountTheme": "keycloakify-starter",
|
"accountTheme": "",
|
||||||
"adminTheme": "",
|
"adminTheme": "",
|
||||||
"emailTheme": "",
|
"emailTheme": "",
|
||||||
"eventsEnabled": false,
|
"eventsEnabled": false,
|
||||||
"eventsListeners": ["jboss-logging"],
|
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||||
"enabledEventTypes": [],
|
"enabledEventTypes": [],
|
||||||
"adminEventsEnabled": false,
|
"adminEventsEnabled": false,
|
||||||
"adminEventsDetailsEnabled": false,
|
"adminEventsDetailsEnabled": false,
|
||||||
@ -1314,14 +1373,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"saml-user-property-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
|
"oidc-full-name-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-address-mapper",
|
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"oidc-full-name-mapper",
|
"saml-user-property-mapper"
|
||||||
"oidc-usermodel-property-mapper"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1370,14 +1429,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-full-name-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-full-name-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-property-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-property-mapper"
|
"saml-user-property-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1495,7 +1554,7 @@
|
|||||||
"defaultLocale": "en",
|
"defaultLocale": "en",
|
||||||
"authenticationFlows": [
|
"authenticationFlows": [
|
||||||
{
|
{
|
||||||
"id": "19317acb-fe8e-4c79-82bc-90e159273075",
|
"id": "30a878f0-57aa-4d20-bab0-6cf1d7317a5c",
|
||||||
"alias": "Account verification options",
|
"alias": "Account verification options",
|
||||||
"description": "Method with which to verity the existing account",
|
"description": "Method with which to verity the existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1521,7 +1580,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "122857d2-33da-4086-8acb-cb0e303aaf1b",
|
"id": "d386affe-d1fe-472a-bee6-54105d0101f5",
|
||||||
"alias": "Authentication Options",
|
"alias": "Authentication Options",
|
||||||
"description": "Authentication options.",
|
"description": "Authentication options.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1555,7 +1614,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "abf5dd35-4791-4268-a10c-5f4b6a06b84a",
|
"id": "77b95bc0-bd0c-46b7-8240-3182023e9d50",
|
||||||
"alias": "Browser - Conditional OTP",
|
"alias": "Browser - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1581,7 +1640,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a18daeec-a33c-4a43-b014-10c84ec69b81",
|
"id": "bc96d3d6-29a1-42af-a63e-bb67a8c6d78f",
|
||||||
"alias": "Direct Grant - Conditional OTP",
|
"alias": "Direct Grant - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1607,7 +1666,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e9f032a7-32f7-457c-becf-011a1a35cc6a",
|
"id": "7697ca74-5c2b-45ab-9335-e0f6dec59b5c",
|
||||||
"alias": "First broker login - Conditional OTP",
|
"alias": "First broker login - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1633,7 +1692,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "9db65b7c-98ca-4003-beea-611038831ffe",
|
"id": "534cb120-f600-4f40-9707-7b781bdbce48",
|
||||||
"alias": "Handle Existing Account",
|
"alias": "Handle Existing Account",
|
||||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1659,7 +1718,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7bd0854c-d7ae-43d7-a1ae-7b759a34cb1d",
|
"id": "f884b048-b223-4ed6-ae16-e49a4255131e",
|
||||||
"alias": "Reset - Conditional OTP",
|
"alias": "Reset - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1685,7 +1744,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2de1a450-fe98-443a-9c6c-d24d8a7ebcb3",
|
"id": "61c7966c-ad72-49f5-84dd-376152348092",
|
||||||
"alias": "User creation or linking",
|
"alias": "User creation or linking",
|
||||||
"description": "Flow for the existing/non-existing user alternatives",
|
"description": "Flow for the existing/non-existing user alternatives",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1712,7 +1771,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7b3efad5-4b7d-4385-a41c-fecc73afdcc4",
|
"id": "72412d0f-dd1b-49fe-bb0b-9dad99eb0491",
|
||||||
"alias": "Verify Existing Account by Re-authentication",
|
"alias": "Verify Existing Account by Re-authentication",
|
||||||
"description": "Reauthentication of existing account",
|
"description": "Reauthentication of existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1738,7 +1797,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "de93418e-8f28-4099-b15e-ad36ec194796",
|
"id": "6b76613e-0d39-440d-aab4-98eaffb1e96a",
|
||||||
"alias": "browser",
|
"alias": "browser",
|
||||||
"description": "browser based authentication",
|
"description": "browser based authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1780,7 +1839,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "0dd3345c-6e82-4c3a-a39a-d49ae1f5c409",
|
"id": "0ff60395-fa89-41be-ad22-fab339e67c49",
|
||||||
"alias": "clients",
|
"alias": "clients",
|
||||||
"description": "Base authentication for clients",
|
"description": "Base authentication for clients",
|
||||||
"providerId": "client-flow",
|
"providerId": "client-flow",
|
||||||
@ -1822,7 +1881,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "87fb4dd0-5326-47a1-b670-982f4872ff89",
|
"id": "bbb3ece7-7dbf-4aba-80c3-dde4b9cdd0b6",
|
||||||
"alias": "direct grant",
|
"alias": "direct grant",
|
||||||
"description": "OpenID Connect Resource Owner Grant",
|
"description": "OpenID Connect Resource Owner Grant",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1856,7 +1915,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "344723b3-4ab1-4999-abdd-32398e82327b",
|
"id": "f5f2c0f6-7dbf-4978-845e-6cacac23aa13",
|
||||||
"alias": "docker auth",
|
"alias": "docker auth",
|
||||||
"description": "Used by Docker clients to authenticate against the IDP",
|
"description": "Used by Docker clients to authenticate against the IDP",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1874,7 +1933,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "f3341938-caf9-4c8a-9cd5-eb34609809ab",
|
"id": "cf463104-19e2-41a8-8a53-d3dd30b75344",
|
||||||
"alias": "first broker login",
|
"alias": "first broker login",
|
||||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1901,7 +1960,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ba7b7357-e324-4b71-9bda-f8512a760e02",
|
"id": "b99b60dc-41ad-487d-be69-a2eefa954a9d",
|
||||||
"alias": "forms",
|
"alias": "forms",
|
||||||
"description": "Username, password, otp and other auth forms.",
|
"description": "Username, password, otp and other auth forms.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1927,7 +1986,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "134971e6-bf63-432c-806e-74ca4fb09963",
|
"id": "18731296-2c96-4f98-a884-027e629e4f9d",
|
||||||
"alias": "http challenge",
|
"alias": "http challenge",
|
||||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1953,7 +2012,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6ea9e2cf-5684-4c65-8c07-930d1cbb0b46",
|
"id": "9a9dce17-5425-4fd5-b3b8-81410e1dbce4",
|
||||||
"alias": "registration",
|
"alias": "registration",
|
||||||
"description": "registration flow",
|
"description": "registration flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1972,7 +2031,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "67e3c8c7-1b5e-4119-84a2-e90876293150",
|
"id": "d0a24e08-cb69-4949-9518-50ae7a96ee49",
|
||||||
"alias": "registration form",
|
"alias": "registration form",
|
||||||
"description": "registration form",
|
"description": "registration form",
|
||||||
"providerId": "form-flow",
|
"providerId": "form-flow",
|
||||||
@ -2014,7 +2073,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fc6d48ec-a1f1-41b1-9310-54f58861d5aa",
|
"id": "6a9aa554-afba-487f-9c82-e94c81c15b3b",
|
||||||
"alias": "reset credentials",
|
"alias": "reset credentials",
|
||||||
"description": "Reset credentials for a user if they forgot their password or something",
|
"description": "Reset credentials for a user if they forgot their password or something",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2056,7 +2115,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "80b1d464-c2ec-4eb1-82e8-32cbede779a8",
|
"id": "e0361d46-eab4-41a6-bb2e-1dc6a5a6b073",
|
||||||
"alias": "saml ecp",
|
"alias": "saml ecp",
|
||||||
"description": "SAML ECP Profile Authentication Flow",
|
"description": "SAML ECP Profile Authentication Flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2076,14 +2135,14 @@
|
|||||||
],
|
],
|
||||||
"authenticatorConfig": [
|
"authenticatorConfig": [
|
||||||
{
|
{
|
||||||
"id": "86b1d5fa-450c-40d8-899c-725861ac39fc",
|
"id": "053d6017-e54c-418a-abe7-44dd4752eacb",
|
||||||
"alias": "create unique user config",
|
"alias": "create unique user config",
|
||||||
"config": {
|
"config": {
|
||||||
"require.password.update.after.registration": "false"
|
"require.password.update.after.registration": "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ea724f02-029a-493d-b4d3-08972be21cfb",
|
"id": "8b545cf4-ab9e-4226-b3c0-d7ac773eae2f",
|
||||||
"alias": "review profile config",
|
"alias": "review profile config",
|
||||||
"config": {
|
"config": {
|
||||||
"update.profile.on.first.login": "missing"
|
"update.profile.on.first.login": "missing"
|
@ -73,7 +73,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["delete-account", "view-profile", "manage-account"]
|
"account": ["view-profile", "manage-account", "delete-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -408,9 +408,9 @@
|
|||||||
"otpPolicyPeriod": 30,
|
"otpPolicyPeriod": 30,
|
||||||
"otpPolicyCodeReusable": false,
|
"otpPolicyCodeReusable": false,
|
||||||
"otpSupportedApplications": [
|
"otpSupportedApplications": [
|
||||||
"totpAppGoogleName",
|
|
||||||
"totpAppFreeOTPName",
|
"totpAppFreeOTPName",
|
||||||
"totpAppMicrosoftAuthenticatorName"
|
"totpAppMicrosoftAuthenticatorName",
|
||||||
|
"totpAppGoogleName"
|
||||||
],
|
],
|
||||||
"webAuthnPolicyRpEntityName": "keycloak",
|
"webAuthnPolicyRpEntityName": "keycloak",
|
||||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||||
@ -456,6 +456,40 @@
|
|||||||
"disableableCredentialTypes": [],
|
"disableableCredentialTypes": [],
|
||||||
"requiredActions": [],
|
"requiredActions": [],
|
||||||
"realmRoles": ["default-roles-myrealm"],
|
"realmRoles": ["default-roles-myrealm"],
|
||||||
|
"clientRoles": {
|
||||||
|
"realm-management": [
|
||||||
|
"create-client",
|
||||||
|
"view-identity-providers",
|
||||||
|
"manage-realm",
|
||||||
|
"query-groups",
|
||||||
|
"manage-clients",
|
||||||
|
"query-users",
|
||||||
|
"realm-admin",
|
||||||
|
"view-authorization",
|
||||||
|
"view-events",
|
||||||
|
"view-clients",
|
||||||
|
"view-realm",
|
||||||
|
"manage-events",
|
||||||
|
"query-realms",
|
||||||
|
"query-clients",
|
||||||
|
"manage-identity-providers",
|
||||||
|
"manage-users",
|
||||||
|
"view-users",
|
||||||
|
"impersonation",
|
||||||
|
"manage-authorization"
|
||||||
|
],
|
||||||
|
"broker": ["read-token"],
|
||||||
|
"account": [
|
||||||
|
"view-profile",
|
||||||
|
"manage-account-links",
|
||||||
|
"view-applications",
|
||||||
|
"manage-consent",
|
||||||
|
"delete-account",
|
||||||
|
"manage-account",
|
||||||
|
"view-groups",
|
||||||
|
"view-consent"
|
||||||
|
]
|
||||||
|
},
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"groups": []
|
"groups": []
|
||||||
}
|
}
|
||||||
@ -521,8 +555,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/realms/myrealm/account/*"],
|
"redirectUris": [
|
||||||
"webOrigins": [],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/realms/myrealm/account/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -657,7 +695,6 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
"login_theme": "keycloakify-starter",
|
|
||||||
"post.logout.redirect.uris": "+",
|
"post.logout.redirect.uris": "+",
|
||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
@ -718,8 +755,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/admin/myrealm/console/*"],
|
"redirectUris": [
|
||||||
"webOrigins": ["+"],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/admin/myrealm/console/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -738,6 +779,24 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -1298,11 +1357,11 @@
|
|||||||
},
|
},
|
||||||
"smtpServer": {},
|
"smtpServer": {},
|
||||||
"loginTheme": "keycloakify-starter",
|
"loginTheme": "keycloakify-starter",
|
||||||
"accountTheme": "keycloakify-starter",
|
"accountTheme": "",
|
||||||
"adminTheme": "",
|
"adminTheme": "",
|
||||||
"emailTheme": "",
|
"emailTheme": "",
|
||||||
"eventsEnabled": false,
|
"eventsEnabled": false,
|
||||||
"eventsListeners": ["jboss-logging"],
|
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||||
"enabledEventTypes": [],
|
"enabledEventTypes": [],
|
||||||
"adminEventsEnabled": false,
|
"adminEventsEnabled": false,
|
||||||
"adminEventsDetailsEnabled": false,
|
"adminEventsDetailsEnabled": false,
|
||||||
@ -1318,13 +1377,13 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"oidc-full-name-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
|
"oidc-full-name-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"oidc-address-mapper"
|
"oidc-address-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1374,14 +1433,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"oidc-address-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"oidc-usermodel-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"saml-user-attribute-mapper",
|
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"saml-user-property-mapper"
|
"saml-user-attribute-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
|
"oidc-full-name-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
2201
src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-22.json
Normal file
2201
src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-22.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -55,7 +55,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["delete-account", "view-profile", "manage-account"]
|
"account": ["view-profile", "delete-account", "manage-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -459,6 +459,40 @@
|
|||||||
"disableableCredentialTypes": [],
|
"disableableCredentialTypes": [],
|
||||||
"requiredActions": [],
|
"requiredActions": [],
|
||||||
"realmRoles": ["default-roles-myrealm"],
|
"realmRoles": ["default-roles-myrealm"],
|
||||||
|
"clientRoles": {
|
||||||
|
"realm-management": [
|
||||||
|
"query-clients",
|
||||||
|
"manage-identity-providers",
|
||||||
|
"create-client",
|
||||||
|
"view-users",
|
||||||
|
"query-groups",
|
||||||
|
"view-realm",
|
||||||
|
"manage-authorization",
|
||||||
|
"view-authorization",
|
||||||
|
"query-users",
|
||||||
|
"impersonation",
|
||||||
|
"realm-admin",
|
||||||
|
"manage-users",
|
||||||
|
"view-identity-providers",
|
||||||
|
"manage-realm",
|
||||||
|
"manage-clients",
|
||||||
|
"query-realms",
|
||||||
|
"view-events",
|
||||||
|
"manage-events",
|
||||||
|
"view-clients"
|
||||||
|
],
|
||||||
|
"broker": ["read-token"],
|
||||||
|
"account": [
|
||||||
|
"manage-account",
|
||||||
|
"view-consent",
|
||||||
|
"view-groups",
|
||||||
|
"delete-account",
|
||||||
|
"view-applications",
|
||||||
|
"manage-account-links",
|
||||||
|
"view-profile",
|
||||||
|
"manage-consent"
|
||||||
|
]
|
||||||
|
},
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"groups": []
|
"groups": []
|
||||||
}
|
}
|
||||||
@ -505,7 +539,6 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
"login_theme": "keycloakify-starter",
|
|
||||||
"post.logout.redirect.uris": "+",
|
"post.logout.redirect.uris": "+",
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
@ -532,8 +565,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/realms/myrealm/account/*"],
|
"redirectUris": [
|
||||||
"webOrigins": [],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/realms/myrealm/account/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -649,7 +686,11 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["https://my-theme.keycloakify.dev/*", "http://localhost*"],
|
"redirectUris": [
|
||||||
|
"https://my-theme.keycloakify.dev/*",
|
||||||
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*"
|
||||||
|
],
|
||||||
"webOrigins": ["*"],
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
@ -664,8 +705,7 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
"login_theme": "keycloakify-starter",
|
"post.logout.redirect.uris": "+",
|
||||||
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*",
|
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
"backchannel.logout.revoke.offline.tokens": "false"
|
"backchannel.logout.revoke.offline.tokens": "false"
|
||||||
@ -725,8 +765,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/admin/myrealm/console/*"],
|
"redirectUris": [
|
||||||
"webOrigins": ["+"],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/admin/myrealm/console/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -745,6 +789,24 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "59cde7ae-2218-4a8e-83af-cad992c3a700",
|
"id": "59cde7ae-2218-4a8e-83af-cad992c3a700",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -1336,12 +1398,12 @@
|
|||||||
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
|
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
|
||||||
},
|
},
|
||||||
"smtpServer": {},
|
"smtpServer": {},
|
||||||
"loginTheme": "",
|
"loginTheme": "keycloakify-starter",
|
||||||
"accountTheme": "keycloakify-starter",
|
"accountTheme": "",
|
||||||
"adminTheme": "",
|
"adminTheme": "",
|
||||||
"emailTheme": "",
|
"emailTheme": "",
|
||||||
"eventsEnabled": false,
|
"eventsEnabled": false,
|
||||||
"eventsListeners": ["jboss-logging"],
|
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||||
"enabledEventTypes": [],
|
"enabledEventTypes": [],
|
||||||
"adminEventsEnabled": false,
|
"adminEventsEnabled": false,
|
||||||
"adminEventsDetailsEnabled": false,
|
"adminEventsDetailsEnabled": false,
|
||||||
@ -1358,13 +1420,13 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"saml-user-property-mapper",
|
|
||||||
"oidc-address-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-usermodel-property-mapper"
|
"oidc-full-name-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"saml-role-list-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1433,14 +1495,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"oidc-address-mapper",
|
|
||||||
"saml-user-attribute-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
"oidc-usermodel-property-mapper"
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"saml-user-attribute-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
|
"oidc-full-name-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -468,6 +468,40 @@
|
|||||||
"disableableCredentialTypes": [],
|
"disableableCredentialTypes": [],
|
||||||
"requiredActions": [],
|
"requiredActions": [],
|
||||||
"realmRoles": ["default-roles-myrealm"],
|
"realmRoles": ["default-roles-myrealm"],
|
||||||
|
"clientRoles": {
|
||||||
|
"realm-management": [
|
||||||
|
"manage-clients",
|
||||||
|
"manage-users",
|
||||||
|
"view-identity-providers",
|
||||||
|
"view-users",
|
||||||
|
"impersonation",
|
||||||
|
"manage-identity-providers",
|
||||||
|
"query-users",
|
||||||
|
"query-realms",
|
||||||
|
"realm-admin",
|
||||||
|
"view-events",
|
||||||
|
"view-realm",
|
||||||
|
"manage-events",
|
||||||
|
"manage-authorization",
|
||||||
|
"manage-realm",
|
||||||
|
"query-clients",
|
||||||
|
"query-groups",
|
||||||
|
"view-clients",
|
||||||
|
"create-client",
|
||||||
|
"view-authorization"
|
||||||
|
],
|
||||||
|
"broker": ["read-token"],
|
||||||
|
"account": [
|
||||||
|
"manage-consent",
|
||||||
|
"manage-account-links",
|
||||||
|
"view-applications",
|
||||||
|
"view-consent",
|
||||||
|
"manage-account",
|
||||||
|
"view-profile",
|
||||||
|
"view-groups",
|
||||||
|
"delete-account"
|
||||||
|
]
|
||||||
|
},
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"groups": []
|
"groups": []
|
||||||
}
|
}
|
||||||
@ -514,7 +548,6 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
"login_theme": "keycloakify-starter",
|
|
||||||
"post.logout.redirect.uris": "+",
|
"post.logout.redirect.uris": "+",
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
@ -541,8 +574,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/realms/myrealm/account/*"],
|
"redirectUris": [
|
||||||
"webOrigins": [],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/realms/myrealm/account/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -658,7 +695,11 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["https://my-theme.keycloakify.dev/*", "http://localhost*"],
|
"redirectUris": [
|
||||||
|
"https://my-theme.keycloakify.dev/*",
|
||||||
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*"
|
||||||
|
],
|
||||||
"webOrigins": ["*"],
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
@ -673,8 +714,7 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
"login_theme": "keycloakify-starter",
|
"post.logout.redirect.uris": "+",
|
||||||
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*##http://localhost*",
|
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
"backchannel.logout.revoke.offline.tokens": "false"
|
"backchannel.logout.revoke.offline.tokens": "false"
|
||||||
@ -840,8 +880,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/admin/myrealm/console/*"],
|
"redirectUris": [
|
||||||
"webOrigins": ["+"],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/admin/myrealm/console/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -875,6 +919,24 @@
|
|||||||
"claim.name": "locale",
|
"claim.name": "locale",
|
||||||
"jsonType.label": "String"
|
"jsonType.label": "String"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
|
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
|
||||||
@ -1451,12 +1513,12 @@
|
|||||||
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
|
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
|
||||||
},
|
},
|
||||||
"smtpServer": {},
|
"smtpServer": {},
|
||||||
"loginTheme": "keycloak",
|
"loginTheme": "keycloakify-starter",
|
||||||
"accountTheme": "keycloakify-starter",
|
"accountTheme": "",
|
||||||
"adminTheme": "",
|
"adminTheme": "",
|
||||||
"emailTheme": "",
|
"emailTheme": "",
|
||||||
"eventsEnabled": false,
|
"eventsEnabled": false,
|
||||||
"eventsListeners": ["jboss-logging"],
|
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||||
"enabledEventTypes": [],
|
"enabledEventTypes": [],
|
||||||
"adminEventsEnabled": false,
|
"adminEventsEnabled": false,
|
||||||
"adminEventsDetailsEnabled": false,
|
"adminEventsDetailsEnabled": false,
|
||||||
@ -1501,14 +1563,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
|
"oidc-full-name-mapper",
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"oidc-address-mapper",
|
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"oidc-full-name-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-property-mapper"
|
"oidc-usermodel-property-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1541,13 +1603,13 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-full-name-mapper",
|
||||||
"saml-user-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
|
"saml-user-attribute-mapper",
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"saml-user-attribute-mapper"
|
"saml-user-property-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
@ -538,10 +538,10 @@
|
|||||||
"emailVerified": true,
|
"emailVerified": true,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"additional_emails": ["test.user@protonmail.com", "testuser@hotmail.com"],
|
"additional_emails": ["test.user@protonmail.com", "testuser@hotmail.com"],
|
||||||
"gender": ["prefer_not_to_say"],
|
|
||||||
"favorite_pet": ["cats"],
|
"favorite_pet": ["cats"],
|
||||||
"favourite_pet": ["cat"],
|
"gender": ["prefer_not_to_say"],
|
||||||
"bio": ["Hello I'm Test User and I do not exist."],
|
"bio": ["Hello I'm Test User and I do not exist."],
|
||||||
|
"favourite_pet": ["cat"],
|
||||||
"phone_number": ["1111111111"],
|
"phone_number": ["1111111111"],
|
||||||
"locale": ["en"],
|
"locale": ["en"],
|
||||||
"favorite_media": ["movies", "series"]
|
"favorite_media": ["movies", "series"]
|
||||||
@ -562,6 +562,40 @@
|
|||||||
"disableableCredentialTypes": [],
|
"disableableCredentialTypes": [],
|
||||||
"requiredActions": [],
|
"requiredActions": [],
|
||||||
"realmRoles": ["default-roles-myrealm"],
|
"realmRoles": ["default-roles-myrealm"],
|
||||||
|
"clientRoles": {
|
||||||
|
"realm-management": [
|
||||||
|
"manage-users",
|
||||||
|
"create-client",
|
||||||
|
"view-users",
|
||||||
|
"view-realm",
|
||||||
|
"query-realms",
|
||||||
|
"impersonation",
|
||||||
|
"view-events",
|
||||||
|
"realm-admin",
|
||||||
|
"manage-authorization",
|
||||||
|
"manage-events",
|
||||||
|
"view-authorization",
|
||||||
|
"manage-clients",
|
||||||
|
"query-users",
|
||||||
|
"query-groups",
|
||||||
|
"manage-realm",
|
||||||
|
"query-clients",
|
||||||
|
"manage-identity-providers",
|
||||||
|
"view-clients",
|
||||||
|
"view-identity-providers"
|
||||||
|
],
|
||||||
|
"broker": ["read-token"],
|
||||||
|
"account": [
|
||||||
|
"delete-account",
|
||||||
|
"view-applications",
|
||||||
|
"manage-account",
|
||||||
|
"view-consent",
|
||||||
|
"view-groups",
|
||||||
|
"view-profile",
|
||||||
|
"manage-account-links",
|
||||||
|
"manage-consent"
|
||||||
|
]
|
||||||
|
},
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"groups": []
|
"groups": []
|
||||||
}
|
}
|
||||||
@ -628,14 +662,16 @@
|
|||||||
"id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3",
|
"id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3",
|
||||||
"clientId": "account-console",
|
"clientId": "account-console",
|
||||||
"name": "${client_account-console}",
|
"name": "${client_account-console}",
|
||||||
|
"description": "",
|
||||||
"rootUrl": "${authBaseUrl}",
|
"rootUrl": "${authBaseUrl}",
|
||||||
|
"adminUrl": "",
|
||||||
"baseUrl": "/realms/myrealm/account/",
|
"baseUrl": "/realms/myrealm/account/",
|
||||||
"surrogateAuthRequired": false,
|
"surrogateAuthRequired": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/realms/myrealm/account/*"],
|
"redirectUris": ["http://localhost*", "http://127.0.0.1*", "*"],
|
||||||
"webOrigins": [],
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -647,8 +683,13 @@
|
|||||||
"frontchannelLogout": false,
|
"frontchannelLogout": false,
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
|
"oidc.ciba.grant.enabled": "false",
|
||||||
|
"backchannel.logout.session.required": "true",
|
||||||
"post.logout.redirect.uris": "+",
|
"post.logout.redirect.uris": "+",
|
||||||
"pkce.code.challenge.method": "S256"
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
|
"display.on.consent.screen": "false",
|
||||||
|
"pkce.code.challenge.method": "S256",
|
||||||
|
"backchannel.logout.revoke.offline.tokens": "false"
|
||||||
},
|
},
|
||||||
"authenticationFlowBindingOverrides": {},
|
"authenticationFlowBindingOverrides": {},
|
||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
@ -791,8 +832,7 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
"login_theme": "keycloakify-starter",
|
"post.logout.redirect.uris": "+",
|
||||||
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*##http://localhost*##http://127.0.0.1*",
|
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
"backchannel.logout.revoke.offline.tokens": "false"
|
"backchannel.logout.revoke.offline.tokens": "false"
|
||||||
@ -885,8 +925,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["/admin/myrealm/console/*"],
|
"redirectUris": [
|
||||||
"webOrigins": ["+"],
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*",
|
||||||
|
"/admin/myrealm/console/*"
|
||||||
|
],
|
||||||
|
"webOrigins": ["*"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
@ -920,6 +964,24 @@
|
|||||||
"claim.name": "locale",
|
"claim.name": "locale",
|
||||||
"jsonType.label": "String"
|
"jsonType.label": "String"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultClientScopes": [
|
"defaultClientScopes": [
|
||||||
@ -1544,11 +1606,11 @@
|
|||||||
},
|
},
|
||||||
"smtpServer": {},
|
"smtpServer": {},
|
||||||
"loginTheme": "keycloakify-starter",
|
"loginTheme": "keycloakify-starter",
|
||||||
"accountTheme": "keycloakify-starter",
|
"accountTheme": "",
|
||||||
"adminTheme": "",
|
"adminTheme": "",
|
||||||
"emailTheme": "",
|
"emailTheme": "",
|
||||||
"eventsEnabled": false,
|
"eventsEnabled": false,
|
||||||
"eventsListeners": ["jboss-logging"],
|
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||||
"enabledEventTypes": [],
|
"enabledEventTypes": [],
|
||||||
"adminEventsEnabled": false,
|
"adminEventsEnabled": false,
|
||||||
"adminEventsDetailsEnabled": false,
|
"adminEventsDetailsEnabled": false,
|
||||||
@ -1574,14 +1636,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
|
"saml-role-list-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"saml-user-property-mapper",
|
"oidc-full-name-mapper"
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1611,14 +1673,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
|
"saml-role-list-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
"saml-role-list-mapper"
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"oidc-full-name-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1672,11 +1734,10 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"privateKey": [
|
"privateKey": [
|
||||||
"MIIEowIBAAKCAQEAsYUWzVfZMd6ywpBmLJYeF1U9Mgd/z3xWvl1Yq76oRPPfpcqQitN+cktWqu0hPerCVSl2ltwXDMrUwFzswG9MiM9hb+BLEld7kYiYkcFNt3lCtmmeRQEae7JwWimzeNV96Qlz0tHY8f9Zh0ffPDsLTN1HGAeRJJhI7mNQm6qCJNMCfVA/O5SWumsIn2XLnSMiQ05AACVHOLUq6rAZ2zCCaYmXTmJkuSOb8e26V303P6l63DSe5HSNXDdI00tjfFFf37q870zhvfsotrjjx0RMijy9Kjj8OZF+pFHpDRaGEi8tpQxZDnCTofTieB/Vp3QP+aTlvAyD3Q1ZnJxGQCLygwIDAQABAoIBABUJ9XMJGNQzamiVwuOWN7ht4UP8ezYvgdEA8NaLUO0PIYVIKyD7l4OwkHPPM9PfRACM2qG0MZp8sCyg4WxIeepy+D979oRqJYUmNRLSipqWlASuItRXIPjiY99uYXdjh2R8Os5pvCD+MZxPX9KHGuaVXmzSJMO7YAAPeYkMHcLYTp/U0c65Ztaaz1zz1FeyvpjkLr9SHiMcIN51zFmhvT1tcRIqy4zidisjrTSUr/KPVxeJtrEfyhTGk3z41yJf5YbeaxaMjJR5x0WXzt1fWVmA/V1bWa2Zlj9d8AxDReA1p7Lpstz34PRoCMj9bmFguI2+RTw6K0D++Jydfxmh8vUCgYEA5Zwk2r3TFO3i3V70LOn6CLzn15yLeuSIJ9p2os70jQOmFMCreLdcUbCaiUe7UV/IIVftbcxhFm9zECXZXX0wubcmHZqyptlbuAn1de4QkLJixXo1A7ZQXBEZk22WN2naXHQF5oK6lh/VSLcZBajTsyvBm5JWXrd8djjG06MugA8CgYEAxexKI5IwcLhpMDV9UPQb/+lDWHVqCT2xwYxnZ85y+5gmrOyyT7mIChz3DFYiaw4CHJWmBkIDBaiDgLEgQk4QXWzYshXawShBHnv1h08bVMMw98Ivec7ZRkV+/ET30YRwC2Uyk4bm4HpwVV5GCFhC4aAvRcCA1CIJk3MwcOwksk0CgYEAqxyaOomMbOR7VQ4WWgJkW26sOHppV8RH06tzDhG9HfnCI2USZHwBSL+b6wKSDiqbMn4cat8M23NjBH2wZ4OMdFqRBS7sRHtnZtfFHYW0wqCuCwzvxTxw1qvHq57Xe6RfHtc4LnjuJELE59PLyfPvEG9jcVS1GREUp+XYBpBtbvECgYAMhWBDU9JAr0noRNoCrw6+Z9Fc3UCyCPcf2XQJOyRHCl8X/XliVchna2GtpB1VTHORv13bc32hdAGtuIbj6vBaGLK0wXEvWw6TkR/9SWHfQOHuKpi6Sf2w1mCsMOjElm5IKkTC1Hvyo4xLukUP7hV9FJcpAH6l7OlSLK1Z13aS2QKBgB6w4gvmVEQruHV5+K60OatuFojr+kxJwmzCb5uKOULUFezT2pA3p3l6IWxGL2XtM+LD0SiZE3KZJUzf+LatYlBU9ek4F1krkVNUTRZpzUa0oADbymCL1chM4oPIs7sISQlFIH2wOSZt6Blvcw0E0wfjd9Gv/LHxcMnlRb1t1sLk"
|
"MIIEowIBAAKCAQEAso89qpvLhf9DIcCb2JAbxItRLSIvP/NCZhMdAExTHyrhM5B27ZQ6MZ7dJQbnMu7QJ7yiClsD1XnDN7Wlj07sY2As3lY3v9kjODBeADYlPuN1m7/fXFHX3qfRT+PwVSaAhMykmqvWp86UTg7t7rNjVBnXPPXItmRLIF+jZUMWQduwNznr6Jh54ZdIwEy4hvX1bpNw0nPl4KXiOi2elvg+rk7BhFywGwQ/HUCGkrcq0XS/aNOy1ChmqDbtq817mYpVeteCDe8xP3MPrZ/s2LiEt4Ip1cNo0dY+a4JwOzwL42h3GaR+80iK3pZNo+Mr0KBOY9GXvdV/MvcPHLQ7VujUGQIDAQABAoIBAAHV0OQwmDxUazqiVGe61Bzmcqs5q03SC1K/FmCi/YVikdskvGLaOmk5UQa4+1uDEq7J30onH9ML8+qeFRQek0rn2ZDfxtBpDqsx7LwTUmQtqc8z6buKQs37db5ctnhlk34UmAotQyDz5wMmCkzWWVUWCT02PdMev5qW/mKuIxaCWLHUFiMJaGrYCCwB/Ra8KLcadKgRbytSUth9qILC4krFfmWtzIx1P6nM1pzQ1nydxNnNPJKjoWtLRJ5b701Y5/h2vAAg6Mr+jKe1DPa9QmAqhQudjGbZ31av+0f1/I+XkflpZfokfU+MrAqNYRTYkevRYgc3wakK5mfVYUiMuOECgYEA7fk55O2OJFsR0Vjy4Dx4eSIwgwobvwEuHxlyWn0RC7nFb00eh6OPuc5sHrOk8bK3P367q67sEhxGyBF16nwxgX/T+c8gTC8QRuwNymosA4Je/zJHbKvyzLGOouCP5gYwq/wUmVWzNApVC7LBfxbsqYyivHABc5xgPmTgecY0VWkCgYEAwBXcUKoyq1KZegyNJcTuwuvBXoYVveFGm6QKKKwzojCCKaR3XXtdSon1qYfuKT0MLxgEDyyBks9DgfCodSsTmajX90Yolhyz3ptcOmRURqTRoJhM4g6qA+Ybd3uy8vAz32RdS+4rCTgnMG/5Xpn5B4ojOnhRcnA2TPCJgWz6QzECgYEAhj1FjD75JMb+mRJNB3L1HpfLt8+28RsQUli/ag4M1Il5txxQsYDxbYXk9biuvezrc/Tglqs43cp3nxpCYwClyIA8KjnN5UvTKb601M7pfx1GyzwokEO61f7/ECAO7FnnkMzFLe3rBdsiOFQg1LkwzT/Y+OVR3E6E+A1dlzPYh6kCgYBIP3CwfnO0cMr9Vv8394x+kEIZFYHT+4mdPOP9TFfXZztuAkhLRv1d7eoSq+fuZuHQTM4qDullmMOhei1CdMNYhmNExIS7gWw+DF1yMQ5py9B1ARPZ6v4TnVczZ7l1GtfH7G4TAy/4tcA3vcYjyPIb3d9GPL8VthMWeVqe7ahr4QKBgEwA7ASbs4NxfBsStEGQYQYAeWOoKnTc50FeYz38O4KrOirtTFPNsJcyCiTE0o4cqu/OebSA5irrauV7SEDl/gfH54g3ZWusQbLt2uMnZYtkd2+Ka3T9XM0QfQW/vYl3eJtdQj89TqzLzyP0AgvAyIgeG3RMH8ojqCh3YKY0FTv/"
|
||||||
],
|
],
|
||||||
"keyUse": ["SIG"],
|
|
||||||
"certificate": [
|
"certificate": [
|
||||||
"MIICnTCCAYUCBgGQBsyplzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGFFs1X2THessKQZiyWHhdVPTIHf898Vr5dWKu+qETz36XKkIrTfnJLVqrtIT3qwlUpdpbcFwzK1MBc7MBvTIjPYW/gSxJXe5GImJHBTbd5QrZpnkUBGnuycFops3jVfekJc9LR2PH/WYdH3zw7C0zdRxgHkSSYSO5jUJuqgiTTAn1QPzuUlrprCJ9ly50jIkNOQAAlRzi1KuqwGdswgmmJl05iZLkjm/Htuld9Nz+petw0nuR0jVw3SNNLY3xRX9+6vO9M4b37KLa448dETIo8vSo4/DmRfqRR6Q0WhhIvLaUMWQ5wk6H04ngf1ad0D/mk5bwMg90NWZycRkAi8oMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAVS+gJshIFX6cmBGI8UaOOI/9+XFb4Gi+DHaHVWVVHTd14MoqNK1bmmyTHbGIZbvK8UqgJ9+FhJX1ejx17d4KBzkZI3tYvPnVacHvaw1CIUMZ1Ini6u+UGUTnIlnQzCG0pcTKjOZXf3ih1B2CKdwyC7XeXyEJHicAIG7XfzYfYd9DYHvA+h6hrXaQcNJMW7WFNbtb3fJhtlv5P1Iw+ZEGdj15ukMI0bg2OEQA0F3jIw6QZpigSAGuai3HOY6OgoPO82d7TyTYlNhuwyutWr9izl6QMc2R7BmRfW9XQj4ICR2VWJiL9nqz+SOyqnjQiOObuw8Vywb8c36R1Ym1aaGjOw=="
|
"MIICnTCCAYUCBgGTy2TGBjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE3MzQ1OVoXDTM0MTIxNTE3MzYzOVowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKPPaqby4X/QyHAm9iQG8SLUS0iLz/zQmYTHQBMUx8q4TOQdu2UOjGe3SUG5zLu0Ce8ogpbA9V5wze1pY9O7GNgLN5WN7/ZIzgwXgA2JT7jdZu/31xR196n0U/j8FUmgITMpJqr1qfOlE4O7e6zY1QZ1zz1yLZkSyBfo2VDFkHbsDc56+iYeeGXSMBMuIb19W6TcNJz5eCl4jotnpb4Pq5OwYRcsBsEPx1AhpK3KtF0v2jTstQoZqg27avNe5mKVXrXgg3vMT9zD62f7Ni4hLeCKdXDaNHWPmuCcDs8C+NodxmkfvNIit6WTaPjK9CgTmPRl73VfzL3Dxy0O1bo1BkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAggzxmYvHqUaCPLxxSidLQMgpu1pTozg3rTq8dcxhcHINI//A/z7qQyDA/QQN5cuSpYvdt2MRWoNop+uRNKqSr3C8aRErbY0j4acl7yG/ghNfQUZ9KxDBxKrd0HLFUibdZobg10+Ih/qXo3Mi2VtkqyZQRl/iy0O3ITgqb7YJUEx5tuEWyGbn+SerFvqZNcmsLziOJefm1n4uqroHgIfmgY6Deh+wZK0DwO3WZ6ThjhMp5GFi1oNeZ9xoExNEXrYp07b2xTQFF57oypc7prf733lqGjPRLfoVJP6qcsjvAlOA7f8TG9sKwGuRsPfadYY9PxmdHxl2k7PHDJeDhA7VdQ=="
|
||||||
],
|
],
|
||||||
"priority": ["100"]
|
"priority": ["100"]
|
||||||
}
|
}
|
||||||
@ -1688,11 +1749,10 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"privateKey": [
|
"privateKey": [
|
||||||
"MIIEogIBAAKCAQEAkQtefHy82e8d5dVWN00LnGI5YmBOTKh0tgqayVRjqLH6u3NfgJVVIe0tFnxa7Wka/ySHrn1KSsW52czZ4uPXLUo4sXBkQxyyFXeZiWN8H+9WiUQ+0hefZF4es5ZPhY2VpeMK9XAnphC362LFLVycXulkpJcQ+4DjI99To4LLyJmjQvsVaJ7amoVJ5xd62eUv+D7f2+jwuaTwjGE3+MWZADXjVxsUY1qJuGLGKnLkNNxJNMDhvnKYw+aa3Z4V90fQVyjN1Volgw3DdA59o4wrWEy+2xHc6j2ESi8+cM60fWzZU9sp2XkyJoCnV7nmwk7pZkDy3zvAkeOWzrr3OWeR3wIDAQABAoIBACWMcet8R0+L7YuATQ+H7IeRjhV/pQWHXp9541RXem1DlgtM9N5Oynk78z4s90Uavphqlo1/deohgdl2hLmODjh1THPzCqGtHhUcnyzICmwiA58JgdHVt7e9/eiz8uY6HxGQ01dyr3D4RwSyzyTNItYXSayqRwU0+phgykA8LhFCAQM/UkRXDf6UCFKBhDyE7VPBaDv0xyxNb7dKtE7C6Qo5t5D40xCfQ8ni8OcD5RvshQq5xOWcw7igxAhlmXCu1fuO2CDiSiqXLMENs4NlwilQ3caMXAIzUiblaKwCrrK2noBoitx6vuOR2tKmIZSlTyDAG4vLQQtOHk53hBoupGECgYEAx4jSmLM9uUzNwNY1zfs8iNswxbU3YibNe2Q+IFmOQofvTaq1jBBxdPWX5ifIbuTvOAA33pmJRh+BtWzOBBQC7Z4i9mdfvyWB6s8t9nnTnWIY5Hj+hV5gaqae59MjdudsORR887fxzPIeAwwaETfKaZnYpC6zLaE3BXwhIcjlFTcCgYEAuhcKf16JkEYNIwanVHpUXjFxwAThAogHWZAngRokmai67Iulx+rSUhhtOIXtmjj/EaObsrqo5yCKAVZ5EbPTOajdd9RtFzH6q3bRjRdp8o8ZVx4c1vMNaOnLbvK4YzJlKSZN9N7m255Mg+/ea3veKVZsSVHDMnuYmH8GjncjPJkCgYAOIUlQmPjZA3BapJDA2nbJ9kO47IFUiQzqHQotPkpNudSfemRK2+s87htoqA6Qk9PA8nsCX3sSJS8JSwA317bxXs55Bo8IOT6/AxbtKmlq7sR2gX78sNdBFjWQkyoixHasgB/tHmyYJ9kqPBQoffvuiH+H+OqlY5JC6CxseQ6H9wKBgF69Hj4MDjLiRwve9k9+2/b8azHcCgX05PEG/+WtPpbwHQIScnseJKdhAjH1lSqf+9OqHLlYaGcK3Nejg42spEvFmcLI5iUZ78lde3++PNUdX0RH81zHbrtL06MPdSojXPcfJi8VUCjdJY1CEFVeQZOACS8mrh7EZ8KzYM4k/055AoGAYqjBv3WS8ul7kAsjpZKpIw1QZZaTjBSmLpjB6X8InF+Zihjgm80Dd4RMFnMnEawhFBvnpklvyw5Ce6NSwcC137kN3NVpJypykkXuYkimg7OxgJjR7YFdbQWJWlc+1eB81WTHcEOHVI/DmeV2yVJcv6kA2iC+3/JA0VoJxvrRBKc="
|
"MIIEowIBAAKCAQEAxoEvnv+YHCqUWANGuku5QYscAZyUE0WHSlcAzZ0bQugPow63piQsuxPz0cpPIuLab6adssXUqKEFheT1H0BqtmT9L/7iOKB6MRuInN4aRzzTH9q02TKPkcpSAzAHTGcsJBMMawlbnIdMu5+mevMPxqeVVxvrnKG27S8H3W5jqIkQw8bo646Hr3l5Dxq/jY7slcSXXXe4ZdefeCvnSqea+fy5c+r/r546nX4FTGiklu6KLQaDc9SfGccrZDmljY7DX1kHrmvIdLShcuukTHc0hi2qbgMcUte/7/svSJLUWOZObKxetd4y1OA49v36xrMqGhwGDdwrWf0VuMBN8eHOCQIDAQABAoIBABz/hUXnFRZURWHKxLvKpnBZPTOiZzfzfxfl4tOmq54CtDoVQyXNq2J+6oOPWC/X+ky3hy+1BQ5x9hJrx+qTU04m2EfOe8da8M7DX28kZlauyjF2loG+MvP7ctn4BluWcip+RTZOYn2DfxBPpRcunR409V+JesoMY7fSwtrfA/Gm0PrXgBK7OuE0nxqFFWnsLOc+HxZECS5r0n1MHEBHe774HkqGcK91j8S+QU+/diTnK+N/ClnKWnabMK8bUO5wAUuKwf2deYkGP91pCEJlVnVZyaXshEM+uxTuMRUlq9h1QAIUatvdQwfOKqZ9XvmTVC8b79qLwmezjoDxNCKbaMMCgYEA71WDpMnA2uS2wCJ/MVwzWGSBDjfeKUPRy33BeUfwLGp4Dro+S1sTrLHgi1HGmvmC8ReZrifUlUHUi3ZHauR6vbNsEoSQ3hplO013kj12EfcBpvKYFg1ODCwevb/JtBTWbDG1P+E9DGiF/2u0aicoJoPolNeNVzgO6YK1OI/S/LMCgYEA1FPTqFPulXxcOK12LgYap8typqJ7zu4fByr42010yrKM+LLNA3bT/i/oRkKc7J1ztKSqlVckADWgK4Y27lI4j1tSgTOxFzwxnTZOeF7ZwGSxq9iy9A84nDiW+m6Hj5RDyBjTSoP2Qqv6d5kTUx+pczZvOVTWRlIEnFETbbxOoFMCgYEA0r1etHx+V4AqtxXpH6KLB5s/1DA3a+hu1BrAgLVqcwGxA27VKW9h7J+YE7UHBzELLpVUWfhyhJa5u6+DhUj4Fw/k6o1WLmvZlZVJ4zhBPeJczw8wAcLnZWp4CybUScBLamt+qGgBZGqpCtZgv1QJU5i09FK0/wa6grz4K3zhEGcCgYAlnGe8xIlZr3rCi2+IvYoROQepHtUhlaqnYWRNrI3IrhIsp7eLKoxo1WGmuHwFqepqEFUrORFmfBlQPGkUlDnyovGdc2OmQwJi39DMn7igzPVwBGXGt7+GZLvRxqx6sX/EPSmIZJHFw6MNdm8m5U/l2bmgBTgjormwWug/IwEmgwKBgEouISIuXsjGxeLmhrOXHKXb6IfKglNJeBM6lTQ6MLaVOso7KdelIntwZNtZwMIi3hlwaUb1X1QmztFbnrvnPhWwJR4ZgMEWanRHthtm0SHzg8EHKT40S91oKabsgHk3wpOvq/iWs+k8qWN4HYp6UO603uLMOfxPYJCFxRtg2TsJ"
|
||||||
],
|
],
|
||||||
"keyUse": ["ENC"],
|
|
||||||
"certificate": [
|
"certificate": [
|
||||||
"MIICnTCCAYUCBgGQBsyq0jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJELXnx8vNnvHeXVVjdNC5xiOWJgTkyodLYKmslUY6ix+rtzX4CVVSHtLRZ8Wu1pGv8kh659SkrFudnM2eLj1y1KOLFwZEMcshV3mYljfB/vVolEPtIXn2ReHrOWT4WNlaXjCvVwJ6YQt+tixS1cnF7pZKSXEPuA4yPfU6OCy8iZo0L7FWie2pqFSecXetnlL/g+39vo8Lmk8IxhN/jFmQA141cbFGNaibhixipy5DTcSTTA4b5ymMPmmt2eFfdH0FcozdVaJYMNw3QOfaOMK1hMvtsR3Oo9hEovPnDOtH1s2VPbKdl5MiaAp1e55sJO6WZA8t87wJHjls669zlnkd8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAD9wQ+CJ0FRgls3JrUzxwHLgrJ3Yo4+mDFpSe1rh2XYK5FEIWDWSqxaXI3p0cOZq75RZmI2xV8oaiJMUz9WMZkbNe/KtGRzHY1N9AZooicGIsnFu1t++b8taFxxpvKWZgnbOum2PZlfcNiXL0QeMv0wwhfn9zKA9W1DRcqYGbIamoyVlumvbNyIjqXJKwGYIOW6GNt7v3wJl5AJw8qAU/O/DQwWwmzcnFGNRxRxAwI7we8EiQ5JlG0Wi+nyAQn74o3RhNr3zsY0ndmFx9bFV4BBo2AiYGozCDOCCG5HvrmoDbrm//wmGRv0tCwueBzWHL2mhtbZ6sGWmMWfiTJ2HPpg=="
|
"MIICnTCCAYUCBgGTy2TG/jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE3MzQ1OVoXDTM0MTIxNTE3MzYzOVowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMaBL57/mBwqlFgDRrpLuUGLHAGclBNFh0pXAM2dG0LoD6MOt6YkLLsT89HKTyLi2m+mnbLF1KihBYXk9R9AarZk/S/+4jigejEbiJzeGkc80x/atNkyj5HKUgMwB0xnLCQTDGsJW5yHTLufpnrzD8anlVcb65yhtu0vB91uY6iJEMPG6OuOh695eQ8av42O7JXEl113uGXXn3gr50qnmvn8uXPq/6+eOp1+BUxopJbuii0Gg3PUnxnHK2Q5pY2Ow19ZB65ryHS0oXLrpEx3NIYtqm4DHFLXv+/7L0iS1FjmTmysXrXeMtTgOPb9+sazKhocBg3cK1n9FbjATfHhzgkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAdUIlJ91E0UkFS45AByjFufRnQbAi1smnHkC3WSN39bhcFT7Hgip97qtABODR58zVHSTS0XcMiL4mMObH3Vyz9J3gmwWZnbokAuo9tYeyrhPh/gqXv3LGtGhTpWlUJ7JEJxH7RVI4UZZyG6Y6FR+3zwiZ0j1p3QsZclfcNmacoi/Ano+4TfloOnY4k8yP7G6LWUTJHpcRNWVVozM3RwekYgpJRAtXDoYfm9p2hRQ090e7NvbblSuVQ/FXhUn4g0wz91WdCWlwXZfvNaRjbynPCHejJpszqiyjPkx3aRKTWqer0ZocKNmY8+RO27XIsXmwOYcjdpX2TCFDv6O+VLfNdw=="
|
||||||
],
|
],
|
||||||
"priority": ["100"],
|
"priority": ["100"],
|
||||||
"algorithm": ["RSA-OAEP"]
|
"algorithm": ["RSA-OAEP"]
|
||||||
@ -1704,8 +1764,8 @@
|
|||||||
"providerId": "aes-generated",
|
"providerId": "aes-generated",
|
||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"kid": ["1c1d0c8a-6f0b-48a9-a66f-488489137d85"],
|
"kid": ["95db7eb8-b57b-475e-90cd-58841a9388d3"],
|
||||||
"secret": ["N4wzheVYYBWxFn9VGWTPQQ"],
|
"secret": ["dp6bv53YrC2PZuJCxa3aNA"],
|
||||||
"priority": ["100"]
|
"priority": ["100"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1715,9 +1775,9 @@
|
|||||||
"providerId": "hmac-generated",
|
"providerId": "hmac-generated",
|
||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"kid": ["ce43821c-6cfd-4ea9-a29a-a724a37e6955"],
|
"kid": ["d0254883-059e-4fdd-bf03-704c76650aab"],
|
||||||
"secret": [
|
"secret": [
|
||||||
"j_8WeQHYt5R6coay0IOUeu9hGvCoJsgnENSoYm0gDlDx6IHOg-f6p17QIaesNmgrzXtJDRpYMhSjpTMHOnHCHLxwUM4eVg9TcszffndB850Yj3PHPeCc5aoHcpYzWN9NDZZ02nBYA04nfbkdlLXiGlpS3I3e502e4DX3rFtbFZ0"
|
"bcW7E4rcbgSKZIQysWOSuhezRGYs5Kzmp3ZESthdTUMyFivK8RbBAdBE4PhFPk5B9TuByDO2RWvd8F7F5YhGJitf6cfYB1BfDuAk-2iBAtdZA98g7a2h4jpwzh-GIgtoRbGbH9qnquUn52f5qteo34g5WifKE2bWjOELza9FrTo"
|
||||||
],
|
],
|
||||||
"priority": ["100"],
|
"priority": ["100"],
|
||||||
"algorithm": ["HS512"]
|
"algorithm": ["HS512"]
|
||||||
@ -2388,7 +2448,7 @@
|
|||||||
"clientSessionMaxLifespan": "0",
|
"clientSessionMaxLifespan": "0",
|
||||||
"organizationsEnabled": "false"
|
"organizationsEnabled": "false"
|
||||||
},
|
},
|
||||||
"keycloakVersion": "25.0.0",
|
"keycloakVersion": "25.0.6",
|
||||||
"userManagedAccessAllowed": false,
|
"userManagedAccessAllowed": false,
|
||||||
"organizationsEnabled": false,
|
"organizationsEnabled": false,
|
||||||
"clientProfiles": {
|
"clientProfiles": {
|
2481
src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-26.json
Normal file
2481
src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-26.json
Normal file
File diff suppressed because it is too large
Load Diff
194
src/bin/start-keycloak/realmConfig/dumpContainerConfig.ts
Normal file
194
src/bin/start-keycloak/realmConfig/dumpContainerConfig.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { CONTAINER_NAME } from "../../shared/constants";
|
||||||
|
import child_process from "child_process";
|
||||||
|
import { join as pathJoin, dirname as pathDirname, basename as pathBasename } from "path";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
|
import { assert, is } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
|
import { type ParsedRealmJson, readRealmJsonFile } from "./ParsedRealmJson";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
cacheDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function dumpContainerConfig(params: {
|
||||||
|
realmName: string;
|
||||||
|
keycloakMajorVersionNumber: number;
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<ParsedRealmJson> {
|
||||||
|
const { realmName, keycloakMajorVersionNumber, buildContext } = params;
|
||||||
|
|
||||||
|
// https://github.com/keycloak/keycloak/issues/33800
|
||||||
|
const doesUseLockedH2Database = keycloakMajorVersionNumber >= 25;
|
||||||
|
|
||||||
|
if (doesUseLockedH2Database) {
|
||||||
|
const dCompleted = new Deferred<void>();
|
||||||
|
|
||||||
|
const cmd = `docker exec ${CONTAINER_NAME} sh -c "cp -rp /opt/keycloak/data/h2 /tmp"`;
|
||||||
|
|
||||||
|
child_process.exec(cmd, error => {
|
||||||
|
if (error !== null) {
|
||||||
|
dCompleted.reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dCompleted.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dCompleted.pr;
|
||||||
|
} catch (error) {
|
||||||
|
assert(is<Error>(error));
|
||||||
|
|
||||||
|
console.log(chalk.red(`Docker command failed: ${cmd}`));
|
||||||
|
|
||||||
|
console.log(chalk.red(error.message));
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const dCompleted = new Deferred<void>();
|
||||||
|
|
||||||
|
const child = child_process.spawn(
|
||||||
|
"docker",
|
||||||
|
[
|
||||||
|
...["exec", CONTAINER_NAME],
|
||||||
|
...["/opt/keycloak/bin/kc.sh", "export"],
|
||||||
|
...["--dir", "/tmp"],
|
||||||
|
...["--realm", realmName],
|
||||||
|
...["--users", "realm_file"],
|
||||||
|
...(!doesUseLockedH2Database
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
...["--db", "dev-file"],
|
||||||
|
...[
|
||||||
|
"--db-url",
|
||||||
|
'"jdbc:h2:file:/tmp/h2/keycloakdb;NON_KEYWORDS=VALUE"'
|
||||||
|
]
|
||||||
|
])
|
||||||
|
],
|
||||||
|
{ shell: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
|
||||||
|
const onExit = (code: number | null) => {
|
||||||
|
dCompleted.reject(
|
||||||
|
new Error(`docker exec kc.sh export command failed with code ${code}`)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
child.once("exit", onExit);
|
||||||
|
|
||||||
|
child.stdout.on("data", data => {
|
||||||
|
const outputStr = data.toString("utf8");
|
||||||
|
|
||||||
|
if (outputStr.includes("Export finished successfully")) {
|
||||||
|
child.removeListener("exit", onExit);
|
||||||
|
|
||||||
|
// NOTE: On older Keycloak versions the process keeps running after the export is done.
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
child.removeListener("exit", onExit2);
|
||||||
|
child.kill();
|
||||||
|
dCompleted.resolve();
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
const onExit2 = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
dCompleted.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
child.once("exit", onExit2);
|
||||||
|
}
|
||||||
|
|
||||||
|
output += outputStr;
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", data => (output += chalk.red(data.toString("utf8"))));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dCompleted.pr;
|
||||||
|
} catch (error) {
|
||||||
|
assert(is<Error>(error));
|
||||||
|
|
||||||
|
console.log(chalk.red(error.message));
|
||||||
|
|
||||||
|
console.log(output);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doesUseLockedH2Database) {
|
||||||
|
const dCompleted = new Deferred<void>();
|
||||||
|
|
||||||
|
const cmd = `docker exec ${CONTAINER_NAME} sh -c "rm -rf /tmp/h2"`;
|
||||||
|
|
||||||
|
child_process.exec(cmd, error => {
|
||||||
|
if (error !== null) {
|
||||||
|
dCompleted.reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dCompleted.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dCompleted.pr;
|
||||||
|
} catch (error) {
|
||||||
|
assert(is<Error>(error));
|
||||||
|
|
||||||
|
console.log(chalk.red(`Docker command failed: ${cmd}`));
|
||||||
|
|
||||||
|
console.log(chalk.red(error.message));
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetRealmConfigJsonFilePath_tmp = pathJoin(
|
||||||
|
buildContext.cacheDirPath,
|
||||||
|
"realm.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const dCompleted = new Deferred<void>();
|
||||||
|
|
||||||
|
const cmd = `docker cp ${CONTAINER_NAME}:/tmp/${realmName}-realm.json ${pathBasename(targetRealmConfigJsonFilePath_tmp)}`;
|
||||||
|
|
||||||
|
child_process.exec(
|
||||||
|
cmd,
|
||||||
|
{
|
||||||
|
cwd: pathDirname(targetRealmConfigJsonFilePath_tmp)
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if (error !== null) {
|
||||||
|
dCompleted.reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dCompleted.resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dCompleted.pr;
|
||||||
|
} catch (error) {
|
||||||
|
assert(is<Error>(error));
|
||||||
|
|
||||||
|
console.log(chalk.red(`Docker command failed: ${cmd}`));
|
||||||
|
|
||||||
|
console.log(chalk.red(error.message));
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return readRealmJsonFile({
|
||||||
|
realmJsonFilePath: targetRealmConfigJsonFilePath_tmp
|
||||||
|
});
|
||||||
|
}
|
1
src/bin/start-keycloak/realmConfig/index.ts
Normal file
1
src/bin/start-keycloak/realmConfig/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./realmConfig";
|
353
src/bin/start-keycloak/realmConfig/prepareRealmConfig.ts
Normal file
353
src/bin/start-keycloak/realmConfig/prepareRealmConfig.ts
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||||
|
import { getDefaultConfig } from "./defaultConfig";
|
||||||
|
import { TEST_APP_URL, type ThemeType, THEME_TYPES } from "../../shared/constants";
|
||||||
|
import { sameFactory } from "evt/tools/inDepth/same";
|
||||||
|
|
||||||
|
export function prepareRealmConfig(params: {
|
||||||
|
parsedRealmJson: ParsedRealmJson;
|
||||||
|
keycloakMajorVersionNumber: number;
|
||||||
|
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||||
|
}): {
|
||||||
|
realmName: string;
|
||||||
|
clientName: string;
|
||||||
|
username: string;
|
||||||
|
} {
|
||||||
|
const { parsedRealmJson, keycloakMajorVersionNumber, parsedKeycloakThemesJsonEntry } =
|
||||||
|
params;
|
||||||
|
|
||||||
|
const { username } = addOrEditTestUser({
|
||||||
|
parsedRealmJson,
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
const { clientId } = addOrEditClient({
|
||||||
|
parsedRealmJson,
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
editAccountConsoleAndSecurityAdminConsole({ parsedRealmJson });
|
||||||
|
|
||||||
|
enableCustomThemes({
|
||||||
|
parsedRealmJson,
|
||||||
|
parsedKeycloakThemesJsonEntry
|
||||||
|
});
|
||||||
|
|
||||||
|
enable_custom_events_listeners: {
|
||||||
|
const name = "keycloakify-logging";
|
||||||
|
|
||||||
|
if (parsedRealmJson.eventsListeners.includes(name)) {
|
||||||
|
break enable_custom_events_listeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedRealmJson.eventsListeners.push(name);
|
||||||
|
|
||||||
|
parsedRealmJson.eventsListeners.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
realmName: parsedRealmJson.realm,
|
||||||
|
clientName: clientId,
|
||||||
|
username
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableCustomThemes(params: {
|
||||||
|
parsedRealmJson: ParsedRealmJson;
|
||||||
|
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||||
|
}) {
|
||||||
|
const { parsedRealmJson, parsedKeycloakThemesJsonEntry } = params;
|
||||||
|
|
||||||
|
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||||
|
parsedRealmJson[`${themeType}Theme` as const] =
|
||||||
|
!parsedKeycloakThemesJsonEntry.types.includes(themeType)
|
||||||
|
? ""
|
||||||
|
: parsedKeycloakThemesJsonEntry.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOrEditTestUser(params: {
|
||||||
|
parsedRealmJson: ParsedRealmJson;
|
||||||
|
keycloakMajorVersionNumber: number;
|
||||||
|
}): { username: string } {
|
||||||
|
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
|
||||||
|
|
||||||
|
const parsedRealmJson_default = getDefaultConfig({ keycloakMajorVersionNumber });
|
||||||
|
|
||||||
|
const [defaultUser_default] = parsedRealmJson_default.users;
|
||||||
|
|
||||||
|
assert(defaultUser_default !== undefined);
|
||||||
|
|
||||||
|
const defaultUser_preexisting = parsedRealmJson.users.find(
|
||||||
|
user => user.username === defaultUser_default.username
|
||||||
|
);
|
||||||
|
|
||||||
|
const newUser = structuredClone(
|
||||||
|
defaultUser_preexisting ??
|
||||||
|
(() => {
|
||||||
|
const firstUser = parsedRealmJson.users[0];
|
||||||
|
|
||||||
|
if (firstUser === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstUserCopy = structuredClone(firstUser);
|
||||||
|
|
||||||
|
firstUserCopy.id = defaultUser_default.id;
|
||||||
|
|
||||||
|
return firstUserCopy;
|
||||||
|
})() ??
|
||||||
|
defaultUser_default
|
||||||
|
);
|
||||||
|
|
||||||
|
newUser.username = defaultUser_default.username;
|
||||||
|
|
||||||
|
delete_existing_password_credential_if_any: {
|
||||||
|
const i = newUser.credentials.findIndex(
|
||||||
|
credential => credential.type === "password"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (i === -1) {
|
||||||
|
break delete_existing_password_credential_if_any;
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser.credentials.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const credential = defaultUser_default.credentials.find(
|
||||||
|
credential => credential.type === "password"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(credential !== undefined);
|
||||||
|
|
||||||
|
newUser.credentials.push(credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const nameByClientId = Object.fromEntries(
|
||||||
|
parsedRealmJson.clients.map(client => [client.id, client.clientId] as const)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newClientRoles: NonNullable<
|
||||||
|
ParsedRealmJson["users"][number]["clientRoles"]
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
for (const clientRole of Object.values(parsedRealmJson.roles.client).flat()) {
|
||||||
|
const clientName = nameByClientId[clientRole.containerId];
|
||||||
|
|
||||||
|
assert(clientName !== undefined);
|
||||||
|
|
||||||
|
(newClientRoles[clientName] ??= []).push(clientRole.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { same: sameSet } = sameFactory({
|
||||||
|
takeIntoAccountArraysOrdering: false
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [clientName, roles] of Object.entries(newClientRoles)) {
|
||||||
|
keep_previous_ordering_if_possible: {
|
||||||
|
const roles_previous = newUser.clientRoles?.[clientName];
|
||||||
|
|
||||||
|
if (roles_previous === undefined) {
|
||||||
|
break keep_previous_ordering_if_possible;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sameSet(roles_previous, roles)) {
|
||||||
|
break keep_previous_ordering_if_possible;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
(newUser.clientRoles ??= {})[clientName] = roles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultUser_preexisting === undefined) {
|
||||||
|
parsedRealmJson.users.push(newUser);
|
||||||
|
} else {
|
||||||
|
const i = parsedRealmJson.users.indexOf(defaultUser_preexisting);
|
||||||
|
assert(i !== -1);
|
||||||
|
parsedRealmJson.users[i] = newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { username: newUser.username };
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOrEditClient(params: {
|
||||||
|
parsedRealmJson: ParsedRealmJson;
|
||||||
|
keycloakMajorVersionNumber: number;
|
||||||
|
}): { clientId: string } {
|
||||||
|
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
|
||||||
|
|
||||||
|
const parsedRealmJson_default = getDefaultConfig({ keycloakMajorVersionNumber });
|
||||||
|
|
||||||
|
const testClient_default = (() => {
|
||||||
|
const clients = parsedRealmJson_default.clients.filter(client => {
|
||||||
|
return JSON.stringify(client).includes(TEST_APP_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(clients.length === 1);
|
||||||
|
|
||||||
|
return clients[0];
|
||||||
|
})();
|
||||||
|
|
||||||
|
const clientIds_builtIn = parsedRealmJson_default.clients
|
||||||
|
.map(client => client.clientId)
|
||||||
|
.filter(clientId => clientId !== testClient_default.clientId);
|
||||||
|
|
||||||
|
const testClient_preexisting = (() => {
|
||||||
|
const clients = parsedRealmJson.clients
|
||||||
|
.filter(client => !clientIds_builtIn.includes(client.clientId))
|
||||||
|
.filter(client => client.protocol === "openid-connect");
|
||||||
|
|
||||||
|
{
|
||||||
|
const client = clients.find(
|
||||||
|
client => client.clientId === testClient_default.clientId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (client !== undefined) {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const client = clients.find(
|
||||||
|
client =>
|
||||||
|
client.redirectUris?.find(redirectUri =>
|
||||||
|
redirectUri.startsWith(TEST_APP_URL)
|
||||||
|
) !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (client !== undefined) {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [client] = clients;
|
||||||
|
|
||||||
|
if (client === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
})();
|
||||||
|
|
||||||
|
let testClient: typeof testClient_default;
|
||||||
|
|
||||||
|
if (testClient_preexisting !== undefined) {
|
||||||
|
testClient = testClient_preexisting;
|
||||||
|
} else {
|
||||||
|
testClient = structuredClone(testClient_default);
|
||||||
|
delete testClient.protocolMappers;
|
||||||
|
parsedRealmJson.clients.push(testClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
testClient.redirectUris = [
|
||||||
|
`${TEST_APP_URL}/*`,
|
||||||
|
"http://localhost*",
|
||||||
|
"http://127.0.0.1*"
|
||||||
|
]
|
||||||
|
.sort()
|
||||||
|
.reverse();
|
||||||
|
|
||||||
|
(testClient.attributes ??= {})["post.logout.redirect.uris"] = "+";
|
||||||
|
|
||||||
|
testClient.webOrigins = ["*"];
|
||||||
|
|
||||||
|
return { clientId: testClient.clientId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function editAccountConsoleAndSecurityAdminConsole(params: {
|
||||||
|
parsedRealmJson: ParsedRealmJson;
|
||||||
|
}) {
|
||||||
|
const { parsedRealmJson } = params;
|
||||||
|
|
||||||
|
for (const clientId of ["account-console", "security-admin-console"] as const) {
|
||||||
|
const client = parsedRealmJson.clients.find(
|
||||||
|
client => client.clientId === clientId
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(client !== undefined);
|
||||||
|
|
||||||
|
{
|
||||||
|
const arr = (client.redirectUris ??= []);
|
||||||
|
|
||||||
|
for (const value of ["http://localhost*", "http://127.0.0.1*"]) {
|
||||||
|
if (!arr.includes(value)) {
|
||||||
|
arr.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.redirectUris?.sort().reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
(client.attributes ??= {})["post.logout.redirect.uris"] = "+";
|
||||||
|
|
||||||
|
client.webOrigins = ["*"];
|
||||||
|
|
||||||
|
admin_specific: {
|
||||||
|
if (clientId !== "security-admin-console") {
|
||||||
|
break admin_specific;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocolMapper_preexisting = client.protocolMappers?.find(
|
||||||
|
protocolMapper => {
|
||||||
|
if (protocolMapper.protocolMapper !== "oidc-hardcoded-claim-mapper") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocolMapper.protocol !== "openid-connect") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocolMapper.config === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocolMapper.config["claim.name"] !== "allowed-origins") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let protocolMapper: NonNullable<typeof protocolMapper_preexisting>;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"claim.value": '["*"]',
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"lightweight.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (protocolMapper_preexisting !== undefined) {
|
||||||
|
protocolMapper = protocolMapper_preexisting;
|
||||||
|
} else {
|
||||||
|
protocolMapper = {
|
||||||
|
id: "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
name: "allowed-origins",
|
||||||
|
protocol: "openid-connect",
|
||||||
|
protocolMapper: "oidc-hardcoded-claim-mapper",
|
||||||
|
consentRequired: false,
|
||||||
|
config
|
||||||
|
};
|
||||||
|
|
||||||
|
(client.protocolMappers ??= []).push(protocolMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(protocolMapper.config !== undefined);
|
||||||
|
|
||||||
|
if (config !== protocolMapper.config) {
|
||||||
|
Object.assign(protocolMapper.config, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
157
src/bin/start-keycloak/realmConfig/realmConfig.ts
Normal file
157
src/bin/start-keycloak/realmConfig/realmConfig.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { getDefaultConfig } from "./defaultConfig";
|
||||||
|
import { prepareRealmConfig } from "./prepareRealmConfig";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import {
|
||||||
|
join as pathJoin,
|
||||||
|
dirname as pathDirname,
|
||||||
|
relative as pathRelative,
|
||||||
|
sep as pathSep
|
||||||
|
} from "path";
|
||||||
|
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||||
|
import {
|
||||||
|
readRealmJsonFile,
|
||||||
|
writeRealmJsonFile,
|
||||||
|
type ParsedRealmJson
|
||||||
|
} from "./ParsedRealmJson";
|
||||||
|
import {
|
||||||
|
dumpContainerConfig,
|
||||||
|
type BuildContextLike as BuildContextLike_dumpContainerConfig
|
||||||
|
} from "./dumpContainerConfig";
|
||||||
|
import * as runExclusive from "run-exclusive";
|
||||||
|
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
||||||
|
import type { ThemeType } from "../../shared/constants";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export type BuildContextLike = BuildContextLike_dumpContainerConfig & {
|
||||||
|
projectDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>;
|
||||||
|
|
||||||
|
export async function getRealmConfig(params: {
|
||||||
|
keycloakMajorVersionNumber: number;
|
||||||
|
realmJsonFilePath_userProvided: string | undefined;
|
||||||
|
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<{
|
||||||
|
realmJsonFilePath: string;
|
||||||
|
clientName: string;
|
||||||
|
realmName: string;
|
||||||
|
username: string;
|
||||||
|
onRealmConfigChange: () => Promise<void>;
|
||||||
|
}> {
|
||||||
|
const {
|
||||||
|
keycloakMajorVersionNumber,
|
||||||
|
realmJsonFilePath_userProvided,
|
||||||
|
parsedKeycloakThemesJsonEntry,
|
||||||
|
buildContext
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const realmJsonFilePath = pathJoin(
|
||||||
|
buildContext.projectDirPath,
|
||||||
|
".keycloakify",
|
||||||
|
`realm-kc-${keycloakMajorVersionNumber}.json`
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedRealmJson = await (async () => {
|
||||||
|
if (realmJsonFilePath_userProvided !== undefined) {
|
||||||
|
return readRealmJsonFile({
|
||||||
|
realmJsonFilePath: realmJsonFilePath_userProvided
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await existsAsync(realmJsonFilePath)) {
|
||||||
|
return readRealmJsonFile({
|
||||||
|
realmJsonFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDefaultConfig({ keycloakMajorVersionNumber });
|
||||||
|
})();
|
||||||
|
|
||||||
|
const { clientName, realmName, username } = prepareRealmConfig({
|
||||||
|
parsedRealmJson,
|
||||||
|
keycloakMajorVersionNumber,
|
||||||
|
parsedKeycloakThemesJsonEntry
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
const dirPath = pathDirname(realmJsonFilePath);
|
||||||
|
|
||||||
|
if (!(await existsAsync(dirPath))) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeRealmJsonFile({
|
||||||
|
realmJsonFilePath,
|
||||||
|
parsedRealmJson,
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
const { onRealmConfigChange } = (() => {
|
||||||
|
const run = runExclusive.build(async () => {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
chalk.grey(`Changes detected to the '${realmName}' config, backing up...`)
|
||||||
|
);
|
||||||
|
|
||||||
|
let parsedRealmJson: ParsedRealmJson;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedRealmJson = await dumpContainerConfig({
|
||||||
|
buildContext,
|
||||||
|
realmName,
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(chalk.red(`Failed to backup '${realmName}' config:`));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeRealmJsonFile({
|
||||||
|
realmJsonFilePath,
|
||||||
|
parsedRealmJson,
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
chalk.grey(
|
||||||
|
`Save changed to \`.${pathSep}${pathRelative(buildContext.projectDirPath, realmJsonFilePath)}\``
|
||||||
|
),
|
||||||
|
chalk.grey(
|
||||||
|
`Next time you'll be running \`keycloakify start-keycloak\`, the realm '${realmName}' will be restored to this state.`
|
||||||
|
),
|
||||||
|
chalk.green(
|
||||||
|
`✓ '${realmName}' config backed up completed in ${Date.now() - start}ms`
|
||||||
|
)
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { waitForDebounce } = waitForDebounceFactory({
|
||||||
|
delay: 1_000
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onRealmConfigChange() {
|
||||||
|
await waitForDebounce();
|
||||||
|
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { onRealmConfigChange };
|
||||||
|
})();
|
||||||
|
|
||||||
|
return {
|
||||||
|
realmJsonFilePath,
|
||||||
|
clientName,
|
||||||
|
realmName,
|
||||||
|
username,
|
||||||
|
onRealmConfigChange
|
||||||
|
};
|
||||||
|
}
|
@ -1,8 +1,12 @@
|
|||||||
import { getBuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import { exclude } from "tsafe/exclude";
|
import { exclude } from "tsafe/exclude";
|
||||||
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
|
import {
|
||||||
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
CONTAINER_NAME,
|
||||||
import { CONTAINER_NAME } from "../shared/constants";
|
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
|
||||||
|
KEYCLOAKIFY_LOGIN_JAR_BASENAME,
|
||||||
|
TEST_APP_URL,
|
||||||
|
ThemeType
|
||||||
|
} from "../shared/constants";
|
||||||
import { SemVer } from "../tools/SemVer";
|
import { SemVer } from "../tools/SemVer";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
@ -10,8 +14,7 @@ import {
|
|||||||
join as pathJoin,
|
join as pathJoin,
|
||||||
relative as pathRelative,
|
relative as pathRelative,
|
||||||
sep as pathSep,
|
sep as pathSep,
|
||||||
basename as pathBasename,
|
basename as pathBasename
|
||||||
dirname as pathDirname
|
|
||||||
} from "path";
|
} from "path";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@ -28,14 +31,20 @@ import { isInside } from "../tools/isInside";
|
|||||||
import { existsAsync } from "../tools/fs.existsAsync";
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
import { rm } from "../tools/fs.rm";
|
import { rm } from "../tools/fs.rm";
|
||||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||||
|
import { startViteDevServer } from "./startViteDevServer";
|
||||||
|
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
||||||
|
import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags";
|
||||||
|
import { getRealmConfig } from "./realmConfig";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
export type CliCommandOptions = CliCommandOptions_common & {
|
export async function command(params: {
|
||||||
port: number | undefined;
|
buildContext: BuildContext;
|
||||||
keycloakVersion: string | undefined;
|
cliCommandOptions: {
|
||||||
realmJsonFilePath: string | undefined;
|
port: number | undefined;
|
||||||
};
|
keycloakVersion: string | undefined;
|
||||||
|
realmJsonFilePath: string | undefined;
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
};
|
||||||
|
}) {
|
||||||
exit_if_docker_not_installed: {
|
exit_if_docker_not_installed: {
|
||||||
let commandOutput: string | undefined = undefined;
|
let commandOutput: string | undefined = undefined;
|
||||||
|
|
||||||
@ -44,11 +53,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
.execSync("docker --version", {
|
.execSync("docker --version", {
|
||||||
stdio: ["ignore", "pipe", "ignore"]
|
stdio: ["ignore", "pipe", "ignore"]
|
||||||
})
|
})
|
||||||
?.toString("utf8");
|
.toString("utf8");
|
||||||
} catch {}
|
} catch {
|
||||||
|
commandOutput = "";
|
||||||
|
}
|
||||||
|
|
||||||
if (commandOutput?.includes("Docker") || commandOutput?.includes("podman")) {
|
commandOutput = commandOutput.trim().toLowerCase();
|
||||||
break exit_if_docker_not_installed;
|
|
||||||
|
for (const term of ["docker", "podman"]) {
|
||||||
|
if (commandOutput.includes(term)) {
|
||||||
|
break exit_if_docker_not_installed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@ -88,13 +103,34 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { cliCommandOptions } = params;
|
const { cliCommandOptions, buildContext } = params;
|
||||||
|
|
||||||
const buildContext = getBuildContext({ cliCommandOptions });
|
const { allSupportedTags, latestMajorTags } = await getSupportedDockerImageTags({
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
const { dockerImageTag } = await (async () => {
|
const { dockerImageTag } = await (async () => {
|
||||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||||
return { dockerImageTag: cliCommandOptions.keycloakVersion };
|
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
|
||||||
|
|
||||||
|
const tag = allSupportedTags.find(tag =>
|
||||||
|
tag.startsWith(cliCommandOptions_keycloakVersion)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tag === undefined) {
|
||||||
|
console.log(
|
||||||
|
chalk.red(
|
||||||
|
[
|
||||||
|
`We could not find a Keycloak Docker image for ${cliCommandOptions_keycloakVersion}`,
|
||||||
|
`Example of valid values: --keycloak-version 26, --keycloak-version 26.0.7`
|
||||||
|
].join("\n")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dockerImageTag: tag };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
|
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
|
||||||
@ -109,45 +145,134 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
"On which version of Keycloak do you want to test your theme?"
|
"On which version of Keycloak do you want to test your theme?"
|
||||||
),
|
),
|
||||||
chalk.gray(
|
chalk.gray(
|
||||||
"You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 25.0.2` (or any other version)"
|
"You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 26` (or any other version)"
|
||||||
)
|
)
|
||||||
].join("\n")
|
].join("\n")
|
||||||
);
|
);
|
||||||
|
|
||||||
const { keycloakVersion } = await promptKeycloakVersion({
|
const tag_userSelected = await (async () => {
|
||||||
startingFromMajor: 18,
|
let tag: string;
|
||||||
excludeMajorVersions: [22],
|
|
||||||
doOmitPatch: true,
|
|
||||||
buildContext
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`→ ${keycloakVersion}`);
|
let latestMajorTags_copy = [...latestMajorTags];
|
||||||
|
|
||||||
return { dockerImageTag: keycloakVersion };
|
while (true) {
|
||||||
|
const { value } = await cliSelect<string>({
|
||||||
|
values: latestMajorTags_copy
|
||||||
|
}).catch(() => {
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
tag = value;
|
||||||
|
|
||||||
|
{
|
||||||
|
const doImplementAccountMpa =
|
||||||
|
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||||
|
buildContext.implementedThemeTypes.account.type === "Multi-Page";
|
||||||
|
|
||||||
|
if (doImplementAccountMpa && tag.startsWith("22.")) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`You are implementing a Multi-Page Account theme. Keycloak 22 is not supported, select another version`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
latestMajorTags_copy = latestMajorTags_copy.filter(
|
||||||
|
tag => !tag.startsWith("22.")
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readMajor = (tag: string) => {
|
||||||
|
const major = parseInt(tag.split(".")[0]);
|
||||||
|
assert(!isNaN(major));
|
||||||
|
return major;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const major = readMajor(tag);
|
||||||
|
|
||||||
|
const doImplementAdminTheme =
|
||||||
|
buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
|
|
||||||
|
const getIsSupported = (major: number) => major >= 23;
|
||||||
|
|
||||||
|
if (doImplementAdminTheme && !getIsSupported(major)) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`You are implementing an Admin theme. Only Keycloak 23 and later are supported, select another version`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
|
||||||
|
getIsSupported(readMajor(tag))
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const doImplementAccountSpa =
|
||||||
|
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||||
|
buildContext.implementedThemeTypes.account.type === "Single-Page";
|
||||||
|
|
||||||
|
const major = readMajor(tag);
|
||||||
|
|
||||||
|
const getIsSupported = (major: number) => major >= 19;
|
||||||
|
|
||||||
|
if (doImplementAccountSpa && !getIsSupported(major)) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`You are implementing a Single-Page Account theme. Only Keycloak 19 and later are supported, select another version`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
|
||||||
|
getIsSupported(readMajor(tag))
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log(`→ ${tag_userSelected}`);
|
||||||
|
|
||||||
|
return { dockerImageTag: tag_userSelected };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const keycloakMajorVersionNumber = (() => {
|
const keycloakMajorVersionNumber = (() => {
|
||||||
if (buildContext.startKeycloakOptions.dockerImage === undefined) {
|
const [wrap] = getSupportedKeycloakMajorVersions()
|
||||||
return SemVer.parse(dockerImageTag).major;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tag } = buildContext.startKeycloakOptions.dockerImage;
|
|
||||||
|
|
||||||
const [wrap] = [18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
|
|
||||||
.map(majorVersionNumber => ({
|
.map(majorVersionNumber => ({
|
||||||
majorVersionNumber,
|
majorVersionNumber,
|
||||||
index: tag.indexOf(`${majorVersionNumber}`)
|
index: dockerImageTag.indexOf(`${majorVersionNumber}`)
|
||||||
}))
|
}))
|
||||||
.filter(({ index }) => index !== -1)
|
.filter(({ index }) => index !== -1)
|
||||||
.sort((a, b) => a.index - b.index);
|
.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
if (wrap === undefined) {
|
if (wrap === undefined) {
|
||||||
console.warn(
|
try {
|
||||||
chalk.yellow(
|
const version = SemVer.parse(dockerImageTag);
|
||||||
`Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25`
|
|
||||||
)
|
console.error(
|
||||||
);
|
chalk.yellow(
|
||||||
return 25;
|
`Keycloak version ${version.major} is not supported, supported versions are ${getSupportedKeycloakMajorVersions().join(", ")}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
} catch {
|
||||||
|
// NOTE: Latest version
|
||||||
|
const [n] = getSupportedKeycloakMajorVersions();
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(
|
||||||
|
`Could not determine the major Keycloak version number from the docker image tag ${dockerImageTag}. Assuming ${n}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrap.majorVersionNumber;
|
return wrap.majorVersionNumber;
|
||||||
@ -190,159 +315,67 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
assert(jarFilePath !== undefined);
|
assert(jarFilePath !== undefined);
|
||||||
|
|
||||||
const extensionJarFilePaths = await Promise.all(
|
const extensionJarFilePaths = [
|
||||||
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
|
...(keycloakMajorVersionNumber <= 20
|
||||||
switch (extensionJar.type) {
|
? (console.log(
|
||||||
case "path": {
|
chalk.yellow(
|
||||||
assert(
|
"WARNING: With older version of keycloak your changes to the realm configuration are not persisted"
|
||||||
await existsAsync(extensionJar.path),
|
)
|
||||||
`${extensionJar.path} does not exist`
|
),
|
||||||
);
|
[])
|
||||||
return extensionJar.path;
|
: [
|
||||||
|
pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
"start-keycloak",
|
||||||
|
KEYCLOAKIFY_LOGIN_JAR_BASENAME
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
...(await Promise.all(
|
||||||
|
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
|
||||||
|
switch (extensionJar.type) {
|
||||||
|
case "path": {
|
||||||
|
assert(
|
||||||
|
await existsAsync(extensionJar.path),
|
||||||
|
`${extensionJar.path} does not exist`
|
||||||
|
);
|
||||||
|
return extensionJar.path;
|
||||||
|
}
|
||||||
|
case "url": {
|
||||||
|
const { archiveFilePath } = await downloadAndExtractArchive({
|
||||||
|
cacheDirPath: buildContext.cacheDirPath,
|
||||||
|
fetchOptions: buildContext.fetchOptions,
|
||||||
|
url: extensionJar.url,
|
||||||
|
uniqueIdOfOnArchiveFile: "no extraction",
|
||||||
|
onArchiveFile: async () => {}
|
||||||
|
});
|
||||||
|
return archiveFilePath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "url": {
|
assert<Equals<typeof extensionJar, never>>(false);
|
||||||
const { archiveFilePath } = await downloadAndExtractArchive({
|
})
|
||||||
cacheDirPath: buildContext.cacheDirPath,
|
))
|
||||||
fetchOptions: buildContext.fetchOptions,
|
];
|
||||||
url: extensionJar.url,
|
|
||||||
uniqueIdOfOnArchiveFile: "no extraction",
|
|
||||||
onArchiveFile: async () => {}
|
|
||||||
});
|
|
||||||
return archiveFilePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert<Equals<typeof extensionJar, never>>(false);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const getRealmJsonFilePath_defaultForKeycloakMajor = (
|
let parsedKeycloakThemesJson = id<
|
||||||
keycloakMajorVersionNumber: number
|
{ themes: { name: string; types: (ThemeType | "email")[] }[] } | undefined
|
||||||
) =>
|
>(undefined);
|
||||||
pathJoin(
|
|
||||||
getThisCodebaseRootDirPath(),
|
|
||||||
"src",
|
|
||||||
"bin",
|
|
||||||
"start-keycloak",
|
|
||||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
|
||||||
);
|
|
||||||
|
|
||||||
const realmJsonFilePath = await (async () => {
|
|
||||||
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
|
||||||
if (cliCommandOptions.realmJsonFilePath === "none") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return getAbsoluteAndInOsFormatPath({
|
|
||||||
pathIsh: cliCommandOptions.realmJsonFilePath,
|
|
||||||
cwd: process.cwd()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
|
|
||||||
assert(
|
|
||||||
await existsAsync(buildContext.startKeycloakOptions.realmJsonFilePath),
|
|
||||||
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
|
|
||||||
);
|
|
||||||
return buildContext.startKeycloakOptions.realmJsonFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const internalFilePath = await (async () => {
|
|
||||||
const defaultFilePath = getRealmJsonFilePath_defaultForKeycloakMajor(
|
|
||||||
keycloakMajorVersionNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fs.existsSync(defaultFilePath)) {
|
|
||||||
return defaultFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`${chalk.yellow(
|
|
||||||
`Keycloakify do not have a realm configuration for Keycloak ${keycloakMajorVersionNumber} yet.`
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(chalk.cyan("Select what configuration to use:"));
|
|
||||||
|
|
||||||
const dirPath = pathDirname(defaultFilePath);
|
|
||||||
|
|
||||||
const { value } = await cliSelect<string>({
|
|
||||||
values: [
|
|
||||||
...fs
|
|
||||||
.readdirSync(dirPath)
|
|
||||||
.filter(fileBasename => fileBasename.endsWith(".json")),
|
|
||||||
"none"
|
|
||||||
]
|
|
||||||
}).catch(() => {
|
|
||||||
process.exit(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (value === "none") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathJoin(dirPath, value);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (internalFilePath === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = pathJoin(
|
|
||||||
buildContext.cacheDirPath,
|
|
||||||
pathBasename(internalFilePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
filePath,
|
|
||||||
Buffer.from(
|
|
||||||
fs
|
|
||||||
.readFileSync(internalFilePath)
|
|
||||||
.toString("utf8")
|
|
||||||
.replace(/keycloakify\-starter/g, buildContext.themeNames[0])
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
return filePath;
|
|
||||||
})();
|
|
||||||
|
|
||||||
add_test_user_if_missing: {
|
|
||||||
if (realmJsonFilePath === undefined) {
|
|
||||||
break add_test_user_if_missing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const realm: Record<string, unknown> = JSON.parse(
|
|
||||||
fs.readFileSync(realmJsonFilePath).toString("utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (realm.users !== undefined) {
|
|
||||||
break add_test_user_if_missing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const realmJsonFilePath_internal = (() => {
|
|
||||||
const filePath = getRealmJsonFilePath_defaultForKeycloakMajor(
|
|
||||||
keycloakMajorVersionNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
return getRealmJsonFilePath_defaultForKeycloakMajor(25);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filePath;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const users = JSON.parse(
|
|
||||||
fs.readFileSync(realmJsonFilePath_internal).toString("utf8")
|
|
||||||
).users;
|
|
||||||
|
|
||||||
realm.users = users;
|
|
||||||
|
|
||||||
fs.writeFileSync(realmJsonFilePath, JSON.stringify(realm, null, 2), "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractThemeResourcesFromJar() {
|
async function extractThemeResourcesFromJar() {
|
||||||
await extractArchive({
|
await extractArchive({
|
||||||
archiveFilePath: jarFilePath,
|
archiveFilePath: jarFilePath,
|
||||||
onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => {
|
onArchiveFile: async ({ relativeFilePathInArchive, writeFile, readFile }) => {
|
||||||
|
if (
|
||||||
|
relativeFilePathInArchive ===
|
||||||
|
pathJoin("META-INF", "keycloak-themes.json") &&
|
||||||
|
parsedKeycloakThemesJson === undefined
|
||||||
|
) {
|
||||||
|
parsedKeycloakThemesJson = JSON.parse(
|
||||||
|
(await readFile()).toString("utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
|
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
|
||||||
await writeFile({
|
await writeFile({
|
||||||
filePath: pathJoin(
|
filePath: pathJoin(
|
||||||
@ -364,6 +397,43 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
await extractThemeResourcesFromJar();
|
await extractThemeResourcesFromJar();
|
||||||
|
|
||||||
|
assert(parsedKeycloakThemesJson !== undefined);
|
||||||
|
|
||||||
|
const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } =
|
||||||
|
await getRealmConfig({
|
||||||
|
keycloakMajorVersionNumber,
|
||||||
|
parsedKeycloakThemesJsonEntry: (() => {
|
||||||
|
const entry = parsedKeycloakThemesJson.themes.find(
|
||||||
|
({ name }) => name === buildContext.themeNames[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(entry !== undefined);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
})(),
|
||||||
|
realmJsonFilePath_userProvided: await (async () => {
|
||||||
|
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
||||||
|
return getAbsoluteAndInOsFormatPath({
|
||||||
|
pathIsh: cliCommandOptions.realmJsonFilePath,
|
||||||
|
cwd: process.cwd()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
|
||||||
|
assert(
|
||||||
|
await existsAsync(
|
||||||
|
buildContext.startKeycloakOptions.realmJsonFilePath
|
||||||
|
),
|
||||||
|
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
|
||||||
|
);
|
||||||
|
return buildContext.startKeycloakOptions.realmJsonFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})(),
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
const jarFilePath_cacheDir = pathJoin(
|
const jarFilePath_cacheDir = pathJoin(
|
||||||
buildContext.cacheDirPath,
|
buildContext.cacheDirPath,
|
||||||
pathBasename(jarFilePath)
|
pathBasename(jarFilePath)
|
||||||
@ -377,17 +447,76 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const DEFAULT_PORT = 8080;
|
const port = cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? 8080;
|
||||||
const port =
|
|
||||||
cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
|
const doStartDevServer = (() => {
|
||||||
|
const hasSpaUi =
|
||||||
|
buildContext.implementedThemeTypes.admin.isImplemented ||
|
||||||
|
(buildContext.implementedThemeTypes.account.isImplemented &&
|
||||||
|
buildContext.implementedThemeTypes.account.type === "Single-Page");
|
||||||
|
|
||||||
|
if (!hasSpaUi) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildContext.bundler !== "vite") {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
[
|
||||||
|
`WARNING: Since you are using ${buildContext.bundler} instead of Vite,`,
|
||||||
|
`you'll have to wait serval seconds for the changes you made on your account or admin theme to be reflected in the browser.\n`,
|
||||||
|
`For a better development experience, consider migrating to Vite.`
|
||||||
|
].join(" ")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keycloakMajorVersionNumber < 25) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
[
|
||||||
|
`WARNING: Your account or admin theme can't be tested with hot module replacement on Keycloak ${keycloakMajorVersionNumber}.`,
|
||||||
|
`This mean that you'll have to wait serval seconds for the changes to be reflected in the browser.`,
|
||||||
|
`For a better development experience, select a more recent version of Keycloak.`
|
||||||
|
].join("\n")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})();
|
||||||
|
|
||||||
|
let devServerPort: number | undefined = undefined;
|
||||||
|
|
||||||
|
if (doStartDevServer) {
|
||||||
|
const { port } = await startViteDevServer({ buildContext });
|
||||||
|
|
||||||
|
devServerPort = port;
|
||||||
|
}
|
||||||
|
|
||||||
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
|
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
|
||||||
|
|
||||||
const dockerRunArgs: string[] = [
|
const dockerRunArgs: string[] = [
|
||||||
`-p${SPACE_PLACEHOLDER}${port}:8080`,
|
`-p${SPACE_PLACEHOLDER}${port}:8080`,
|
||||||
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
|
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
|
||||||
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
|
...(keycloakMajorVersionNumber >= 26
|
||||||
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`,
|
? [
|
||||||
|
`-e${SPACE_PLACEHOLDER}KC_BOOTSTRAP_ADMIN_USERNAME=admin`,
|
||||||
|
`-e${SPACE_PLACEHOLDER}KC_BOOTSTRAP_ADMIN_PASSWORD=admin`
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
|
||||||
|
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`
|
||||||
|
]),
|
||||||
|
...(devServerPort === undefined
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
`-e${SPACE_PLACEHOLDER}${KEYCLOAKIFY_SPA_DEV_SERVER_PORT}=${devServerPort}`
|
||||||
|
]),
|
||||||
...(buildContext.startKeycloakOptions.dockerExtraArgs.length === 0
|
...(buildContext.startKeycloakOptions.dockerExtraArgs.length === 0
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
@ -398,12 +527,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
...(realmJsonFilePath === undefined
|
...(realmJsonFilePath === undefined
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
|
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/${realmName}-realm.json`
|
||||||
]),
|
]),
|
||||||
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath_cacheDir)}":/opt/keycloak/providers/keycloak-theme.jar`,
|
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
|
||||||
...extensionJarFilePaths.map(
|
...extensionJarFilePaths.map(
|
||||||
jarFilePath =>
|
jarFilePath =>
|
||||||
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
|
`-v${SPACE_PLACEHOLDER}"${jarFilePath}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
|
||||||
),
|
),
|
||||||
...(keycloakMajorVersionNumber <= 20
|
...(keycloakMajorVersionNumber <= 20
|
||||||
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
|
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
|
||||||
@ -426,7 +555,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
}))
|
}))
|
||||||
.map(
|
.map(
|
||||||
({ localDirPath, containerDirPath }) =>
|
({ localDirPath, containerDirPath }) =>
|
||||||
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
|
`-v${SPACE_PLACEHOLDER}"${localDirPath}":${containerDirPath}:rw`
|
||||||
),
|
),
|
||||||
...buildContext.environmentVariables
|
...buildContext.environmentVariables
|
||||||
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
||||||
@ -473,7 +602,14 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
{ shell: true }
|
{ shell: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
child.stdout.on("data", data => process.stdout.write(data));
|
child.stdout.on("data", async data => {
|
||||||
|
if (data.toString("utf8").includes("keycloakify-logging: REALM_CONFIG_CHANGED")) {
|
||||||
|
await onRealmConfigChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
child.stderr.on("data", data => process.stderr.write(data));
|
child.stderr.on("data", data => process.stderr.write(data));
|
||||||
|
|
||||||
@ -520,9 +656,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
`${chalk.green("Your theme is accessible at:")}`,
|
`${chalk.green("Your theme is accessible at:")}`,
|
||||||
`${chalk.green("➜")} ${chalk.cyan.bold(
|
`${chalk.green("➜")} ${chalk.cyan.bold(
|
||||||
(() => {
|
(() => {
|
||||||
const url = new URL("https://my-theme.keycloakify.dev");
|
const url = new URL(TEST_APP_URL);
|
||||||
|
|
||||||
if (port !== DEFAULT_PORT) {
|
if (port !== 8080) {
|
||||||
url.searchParams.set("port", `${port}`);
|
url.searchParams.set("port", `${port}`);
|
||||||
}
|
}
|
||||||
if (kcHttpRelativePath !== undefined) {
|
if (kcHttpRelativePath !== undefined) {
|
||||||
@ -531,13 +667,20 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
kcHttpRelativePath
|
kcHttpRelativePath
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (realmName !== "myrealm") {
|
||||||
|
url.searchParams.set("realm", realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientName !== "myclient") {
|
||||||
|
url.searchParams.set("client", clientName);
|
||||||
|
}
|
||||||
|
|
||||||
return url.href;
|
return url.href;
|
||||||
})()
|
})()
|
||||||
)}`,
|
)}`,
|
||||||
"",
|
"",
|
||||||
"You can login with the following credentials:",
|
"You can login with the following credentials:",
|
||||||
`- username: ${chalk.cyan.bold("testuser")}`,
|
`- username: ${chalk.cyan.bold(username)}`,
|
||||||
`- password: ${chalk.cyan.bold("password123")}`,
|
`- password: ${chalk.cyan.bold("password123")}`,
|
||||||
"",
|
"",
|
||||||
`Watching for changes in ${chalk.bold(
|
`Watching for changes in ${chalk.bold(
|
||||||
@ -594,6 +737,92 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.on("all", async (...[, filePath]) => {
|
.on("all", async (...[, filePath]) => {
|
||||||
|
ignore_path_covered_by_hmr: {
|
||||||
|
if (filePath.endsWith(".properties")) {
|
||||||
|
break ignore_path_covered_by_hmr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doStartDevServer) {
|
||||||
|
break ignore_path_covered_by_hmr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ignore_account_spa: {
|
||||||
|
const doImplementAccountSpa =
|
||||||
|
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||||
|
buildContext.implementedThemeTypes.account.type ===
|
||||||
|
"Single-Page";
|
||||||
|
|
||||||
|
if (!doImplementAccountSpa) {
|
||||||
|
break ignore_account_spa;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isInside({
|
||||||
|
dirPath: pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"account"
|
||||||
|
),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_account_spa;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ignore_admin: {
|
||||||
|
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
|
||||||
|
break ignore_admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isInside({
|
||||||
|
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ignore_patternfly: {
|
||||||
|
if (
|
||||||
|
!isInside({
|
||||||
|
dirPath: pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"shared",
|
||||||
|
"@patternfly"
|
||||||
|
),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_patternfly;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ignore_keycloak_ui_shared: {
|
||||||
|
if (
|
||||||
|
!isInside({
|
||||||
|
dirPath: pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"shared",
|
||||||
|
"keycloak-ui-shared"
|
||||||
|
),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_keycloak_ui_shared;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Detected changes in ${filePath}`);
|
console.log(`Detected changes in ${filePath}`);
|
||||||
|
|
||||||
await waitForDebounce();
|
await waitForDebounce();
|
||||||
|
67
src/bin/start-keycloak/startViteDevServer.ts
Normal file
67
src/bin/start-keycloak/startViteDevServer.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import * as child_process from "child_process";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
|
||||||
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
projectDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export function startViteDevServer(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<{ port: number }> {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
console.log(chalk.blue(`$ npx vite dev`));
|
||||||
|
|
||||||
|
const child = child_process.spawn("npx", ["vite", "dev"], {
|
||||||
|
cwd: buildContext.projectDirPath,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
[VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES.READ_KC_CONTEXT_FROM_URL]: "true"
|
||||||
|
},
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout.on("data", data => {
|
||||||
|
if (!data.toString("utf8").includes("[vite] hmr")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", data => process.stderr.write(data));
|
||||||
|
|
||||||
|
const dPort = new Deferred<number>();
|
||||||
|
|
||||||
|
{
|
||||||
|
const onData = (data: Buffer) => {
|
||||||
|
//Local: http://localhost:8083/
|
||||||
|
const match = data
|
||||||
|
.toString("utf8")
|
||||||
|
.replace(/\x1b[[0-9;]*m/g, "")
|
||||||
|
.match(/Local:\s*http:\/\/(?:localhost|127\.0\.0\.1):(\d+)\//);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.stdout.off("data", onData);
|
||||||
|
|
||||||
|
const port = parseInt(match[1]);
|
||||||
|
|
||||||
|
assert(!isNaN(port));
|
||||||
|
|
||||||
|
dPort.resolve(port);
|
||||||
|
};
|
||||||
|
|
||||||
|
child.stdout.on("data", onData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dPort.pr.then(port => ({ port }));
|
||||||
|
}
|
319
src/bin/sync-extensions/extensionModuleMeta.ts
Normal file
319
src/bin/sync-extensions/extensionModuleMeta.ts
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
import { assert, type Equals, is } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
import { listInstalledModules } from "../tools/listInstalledModules";
|
||||||
|
import { crawlAsync } from "../tools/crawlAsync";
|
||||||
|
import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
|
||||||
|
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||||
|
import {
|
||||||
|
getExtensionModuleFileSourceCodeReadyToBeCopied,
|
||||||
|
type BuildContextLike as BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied
|
||||||
|
} from "./getExtensionModuleFileSourceCodeReadyToBeCopied";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
|
import { isAmong } from "tsafe/isAmong";
|
||||||
|
|
||||||
|
export type ExtensionModuleMeta = {
|
||||||
|
moduleName: string;
|
||||||
|
version: string;
|
||||||
|
files: {
|
||||||
|
fileRelativePath: string;
|
||||||
|
hash: string;
|
||||||
|
copyableFilePath: string;
|
||||||
|
}[];
|
||||||
|
peerDependencies: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zExtensionModuleMeta = (() => {
|
||||||
|
type ExpectedType = ExtensionModuleMeta;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
moduleName: z.string(),
|
||||||
|
version: z.string(),
|
||||||
|
files: z.array(
|
||||||
|
z.object({
|
||||||
|
fileRelativePath: z.string(),
|
||||||
|
hash: z.string(),
|
||||||
|
copyableFilePath: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
peerDependencies: z.record(z.string())
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<InferredType, ExpectedType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<ExpectedType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
type ParsedCacheFile = {
|
||||||
|
keycloakifyVersion: string;
|
||||||
|
prettierConfigHash: string | null;
|
||||||
|
thisFilePath: string;
|
||||||
|
extensionModuleMetas: ExtensionModuleMeta[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedCacheFile = (() => {
|
||||||
|
type ExpectedType = ParsedCacheFile;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
keycloakifyVersion: z.string(),
|
||||||
|
prettierConfigHash: z.union([z.string(), z.null()]),
|
||||||
|
thisFilePath: z.string(),
|
||||||
|
extensionModuleMetas: z.array(zExtensionModuleMeta)
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<InferredType, ExpectedType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<ExpectedType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const CACHE_FILE_RELATIVE_PATH = pathJoin("extension-modules", "cache.json");
|
||||||
|
|
||||||
|
export type BuildContextLike =
|
||||||
|
BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied & {
|
||||||
|
cacheDirPath: string;
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
projectDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function getExtensionModuleMetas(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<ExtensionModuleMeta[]> {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
|
||||||
|
|
||||||
|
const keycloakifyVersion = readThisNpmPackageVersion();
|
||||||
|
|
||||||
|
const prettierConfigHash = await (async () => {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { configHash } = await getPrettier();
|
||||||
|
|
||||||
|
return configHash;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const installedExtensionModules = await (async () => {
|
||||||
|
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
|
||||||
|
packageJsonFilePath: buildContext.packageJsonFilePath,
|
||||||
|
filter: ({ moduleName }) =>
|
||||||
|
moduleName.includes("keycloakify") && moduleName !== "keycloakify"
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
await Promise.all(
|
||||||
|
installedModulesWithKeycloakifyInTheName.map(async entry => {
|
||||||
|
if (!(await existsAsync(pathJoin(entry.dirPath, KEYCLOAK_THEME)))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).filter(exclude(undefined));
|
||||||
|
})();
|
||||||
|
|
||||||
|
const cacheContent = await (async () => {
|
||||||
|
if (!(await existsAsync(cacheFilePath))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fsPr.readFile(cacheFilePath);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const extensionModuleMetas_cacheUpToDate: ExtensionModuleMeta[] = await (async () => {
|
||||||
|
const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
|
||||||
|
if (cacheContent === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheContentStr = cacheContent.toString("utf8");
|
||||||
|
|
||||||
|
let parsedCacheFile: unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedCacheFile = JSON.parse(cacheContentStr);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
zParsedCacheFile.parse(parsedCacheFile);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(is<ParsedCacheFile>(parsedCacheFile));
|
||||||
|
|
||||||
|
return parsedCacheFile;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (parsedCacheFile === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedCacheFile.keycloakifyVersion !== keycloakifyVersion) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedCacheFile.prettierConfigHash !== prettierConfigHash) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedCacheFile.thisFilePath !== cacheFilePath) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionModuleMetas_cacheUpToDate =
|
||||||
|
parsedCacheFile.extensionModuleMetas.filter(extensionModuleMeta => {
|
||||||
|
const correspondingInstalledExtensionModule =
|
||||||
|
installedExtensionModules.find(
|
||||||
|
installedExtensionModule =>
|
||||||
|
installedExtensionModule.moduleName ===
|
||||||
|
extensionModuleMeta.moduleName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (correspondingInstalledExtensionModule === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
correspondingInstalledExtensionModule.version ===
|
||||||
|
extensionModuleMeta.version
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return extensionModuleMetas_cacheUpToDate;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const extensionModuleMetas = await Promise.all(
|
||||||
|
installedExtensionModules.map(
|
||||||
|
async ({
|
||||||
|
moduleName,
|
||||||
|
version,
|
||||||
|
peerDependencies,
|
||||||
|
dirPath
|
||||||
|
}): Promise<ExtensionModuleMeta> => {
|
||||||
|
use_cache: {
|
||||||
|
const extensionModuleMeta_cache =
|
||||||
|
extensionModuleMetas_cacheUpToDate.find(
|
||||||
|
extensionModuleMeta =>
|
||||||
|
extensionModuleMeta.moduleName === moduleName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (extensionModuleMeta_cache === undefined) {
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensionModuleMeta_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: ExtensionModuleMeta["files"] = [];
|
||||||
|
|
||||||
|
{
|
||||||
|
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
|
||||||
|
|
||||||
|
await crawlAsync({
|
||||||
|
dirPath: srcDirPath,
|
||||||
|
returnedPathsType: "relative to dirPath",
|
||||||
|
onFileFound: async fileRelativePath => {
|
||||||
|
const sourceCode =
|
||||||
|
await getExtensionModuleFileSourceCodeReadyToBeCopied({
|
||||||
|
buildContext,
|
||||||
|
fileRelativePath,
|
||||||
|
isOwnershipAction: false,
|
||||||
|
extensionModuleDirPath: dirPath,
|
||||||
|
extensionModuleName: moduleName,
|
||||||
|
extensionModuleVersion: version
|
||||||
|
});
|
||||||
|
|
||||||
|
const hash = computeHash(sourceCode);
|
||||||
|
|
||||||
|
const copyableFilePath = pathJoin(
|
||||||
|
pathDirname(cacheFilePath),
|
||||||
|
KEYCLOAK_THEME,
|
||||||
|
fileRelativePath
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const dirPath = pathDirname(copyableFilePath);
|
||||||
|
|
||||||
|
if (!(await existsAsync(dirPath))) {
|
||||||
|
await fsPr.mkdir(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fsPr.writeFile(copyableFilePath, sourceCode);
|
||||||
|
|
||||||
|
files.push({
|
||||||
|
fileRelativePath,
|
||||||
|
hash,
|
||||||
|
copyableFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return id<ExtensionModuleMeta>({
|
||||||
|
moduleName,
|
||||||
|
version,
|
||||||
|
files,
|
||||||
|
peerDependencies: Object.fromEntries(
|
||||||
|
Object.entries(peerDependencies).filter(
|
||||||
|
([moduleName]) =>
|
||||||
|
!isAmong(["react", "@types/react"], moduleName)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
update_cache: {
|
||||||
|
const parsedCacheFile = id<ParsedCacheFile>({
|
||||||
|
keycloakifyVersion,
|
||||||
|
prettierConfigHash,
|
||||||
|
thisFilePath: cacheFilePath,
|
||||||
|
extensionModuleMetas
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheContent_new = Buffer.from(
|
||||||
|
JSON.stringify(parsedCacheFile, null, 2),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cacheContent !== undefined && cacheContent_new.equals(cacheContent)) {
|
||||||
|
break update_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
create_dir: {
|
||||||
|
const dirPath = pathDirname(cacheFilePath);
|
||||||
|
|
||||||
|
if (await existsAsync(dirPath)) {
|
||||||
|
break create_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.mkdir(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.writeFile(cacheFilePath, cacheContent_new);
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensionModuleMetas;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeHash(data: Buffer) {
|
||||||
|
return crypto.createHash("sha256").update(data).digest("hex");
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { join as pathJoin, sep as pathSep } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
themeSrcDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function getExtensionModuleFileSourceCodeReadyToBeCopied(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
fileRelativePath: string;
|
||||||
|
isOwnershipAction: boolean;
|
||||||
|
extensionModuleDirPath: string;
|
||||||
|
extensionModuleName: string;
|
||||||
|
extensionModuleVersion: string;
|
||||||
|
}): Promise<Buffer> {
|
||||||
|
const {
|
||||||
|
buildContext,
|
||||||
|
extensionModuleDirPath,
|
||||||
|
fileRelativePath,
|
||||||
|
isOwnershipAction,
|
||||||
|
extensionModuleName,
|
||||||
|
extensionModuleVersion
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
let sourceCode = (
|
||||||
|
await fsPr.readFile(
|
||||||
|
pathJoin(extensionModuleDirPath, KEYCLOAK_THEME, fileRelativePath)
|
||||||
|
)
|
||||||
|
).toString("utf8");
|
||||||
|
|
||||||
|
sourceCode = addCommentToSourceCode({
|
||||||
|
sourceCode,
|
||||||
|
fileRelativePath,
|
||||||
|
commentLines: (() => {
|
||||||
|
const path = fileRelativePath.split(pathSep).join("/");
|
||||||
|
|
||||||
|
return isOwnershipAction
|
||||||
|
? [
|
||||||
|
`This file has been claimed for ownership from ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||||
|
`To relinquish ownership and restore this file to its original content, run the following command:`,
|
||||||
|
``,
|
||||||
|
`$ npx keycloakify own --path "${path}" --revert`
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`WARNING: Before modifying this file, run the following command:`,
|
||||||
|
``,
|
||||||
|
`$ npx keycloakify own --path "${path}"`,
|
||||||
|
``,
|
||||||
|
`This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||||
|
`It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.`
|
||||||
|
];
|
||||||
|
})()
|
||||||
|
});
|
||||||
|
|
||||||
|
const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
|
||||||
|
|
||||||
|
format: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break format;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
filePath: destFilePath,
|
||||||
|
sourceCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(sourceCode, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCommentToSourceCode(params: {
|
||||||
|
sourceCode: string;
|
||||||
|
fileRelativePath: string;
|
||||||
|
commentLines: string[];
|
||||||
|
}): string {
|
||||||
|
const { sourceCode, fileRelativePath, commentLines } = params;
|
||||||
|
|
||||||
|
const toResult = (comment: string) => {
|
||||||
|
return [comment, ``, sourceCode].join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const ext of [".ts", ".tsx", ".css", ".less", ".sass", ".js", ".jsx"]) {
|
||||||
|
if (!fileRelativePath.endsWith(ext)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResult(
|
||||||
|
[`/**`, ...commentLines.map(line => ` * ${line}`), ` */`].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".properties")) {
|
||||||
|
return toResult(commentLines.map(line => `# ${line}`).join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".ftl")) {
|
||||||
|
const comment = [`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join(
|
||||||
|
"\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sourceCode.trim().startsWith("<#ftl")) {
|
||||||
|
const [first, ...rest] = sourceCode.split(">");
|
||||||
|
|
||||||
|
const last = rest.join(">");
|
||||||
|
|
||||||
|
return [`${first}>`, comment, last].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResult(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
|
||||||
|
const comment = [
|
||||||
|
`<!--`,
|
||||||
|
...commentLines.map(
|
||||||
|
line =>
|
||||||
|
` ${line
|
||||||
|
.replace("--path", "-t")
|
||||||
|
.replace("--revert", "-r")
|
||||||
|
.replace("Before modifying", "Before modifying or replacing")}`
|
||||||
|
),
|
||||||
|
`-->`
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".html") && sourceCode.trim().startsWith("<!")) {
|
||||||
|
const [first, ...rest] = sourceCode.split(">");
|
||||||
|
|
||||||
|
const last = rest.join(">");
|
||||||
|
|
||||||
|
return [`${first}>`, comment, last].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".svg") && sourceCode.trim().startsWith("<?")) {
|
||||||
|
const [first, ...rest] = sourceCode.split("?>");
|
||||||
|
|
||||||
|
const last = rest.join("?>");
|
||||||
|
|
||||||
|
return [`${first}?>`, comment, last].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResult(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceCode;
|
||||||
|
}
|
1
src/bin/sync-extensions/index.ts
Normal file
1
src/bin/sync-extensions/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./sync-extension";
|
@ -0,0 +1,159 @@
|
|||||||
|
import { assert, type Equals, is } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import type { ExtensionModuleMeta } from "./extensionModuleMeta";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { SemVer } from "../tools/SemVer";
|
||||||
|
import { same } from "evt/tools/inDepth/same";
|
||||||
|
import { runPrettier, getIsPrettierAvailable } from "../tools/runPrettier";
|
||||||
|
import { npmInstall } from "../tools/npmInstall";
|
||||||
|
import { dirname as pathDirname } from "path";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export type ExtensionModuleMetaLike = {
|
||||||
|
moduleName: string;
|
||||||
|
peerDependencies: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<ExtensionModuleMeta extends ExtensionModuleMetaLike ? true : false>();
|
||||||
|
|
||||||
|
export async function installExtensionModulesPeerDependencies(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
extensionModuleMetas: ExtensionModuleMetaLike[];
|
||||||
|
}): Promise<void | never> {
|
||||||
|
const { buildContext, extensionModuleMetas } = params;
|
||||||
|
|
||||||
|
const { extensionModulesPerDependencies } = (() => {
|
||||||
|
const extensionModulesPerDependencies: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const { peerDependencies } of extensionModuleMetas) {
|
||||||
|
for (const [peerDependencyName, versionRange_candidate] of Object.entries(
|
||||||
|
peerDependencies
|
||||||
|
)) {
|
||||||
|
const versionRange = (() => {
|
||||||
|
const versionRange_current =
|
||||||
|
extensionModulesPerDependencies[peerDependencyName];
|
||||||
|
|
||||||
|
if (versionRange_current === undefined) {
|
||||||
|
return versionRange_candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionRange_current === "*") {
|
||||||
|
return versionRange_candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionRange_candidate === "*") {
|
||||||
|
return versionRange_current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { versionRange } = [
|
||||||
|
versionRange_current,
|
||||||
|
versionRange_candidate
|
||||||
|
]
|
||||||
|
.map(versionRange => ({
|
||||||
|
versionRange,
|
||||||
|
semVer: SemVer.parse(
|
||||||
|
(() => {
|
||||||
|
if (
|
||||||
|
versionRange.startsWith("^") ||
|
||||||
|
versionRange.startsWith("~")
|
||||||
|
) {
|
||||||
|
return versionRange.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionRange;
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
.sort((a, b) => SemVer.compare(b.semVer, a.semVer))[0];
|
||||||
|
|
||||||
|
return versionRange;
|
||||||
|
})();
|
||||||
|
|
||||||
|
extensionModulesPerDependencies[peerDependencyName] = versionRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { extensionModulesPerDependencies };
|
||||||
|
})();
|
||||||
|
|
||||||
|
const parsedPackageJson = await (async () => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
dependencies?: Record<string, string>;
|
||||||
|
devDependencies?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zParsedPackageJson = z.object({
|
||||||
|
dependencies: z.record(z.string()).optional(),
|
||||||
|
devDependencies: z.record(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zParsedPackageJson>;
|
||||||
|
|
||||||
|
assert<Equals<InferredType, TargetType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zParsedPackageJson);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
(await fsPr.readFile(buildContext.packageJsonFilePath)).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
|
||||||
|
|
||||||
|
for (const [moduleName, versionRange] of Object.entries(
|
||||||
|
extensionModulesPerDependencies
|
||||||
|
)) {
|
||||||
|
if (moduleName.startsWith("@types/")) {
|
||||||
|
(parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedPackageJson.devDependencies !== undefined) {
|
||||||
|
delete parsedPackageJson.devDependencies[moduleName];
|
||||||
|
}
|
||||||
|
|
||||||
|
(parsedPackageJson.dependencies ??= {})[moduleName] = versionRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (same(parsedPackageJson, parsedPackageJson_before)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let packageJsonContentStr = JSON.stringify(parsedPackageJson, null, 2);
|
||||||
|
|
||||||
|
format: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break format;
|
||||||
|
}
|
||||||
|
|
||||||
|
packageJsonContentStr = await runPrettier({
|
||||||
|
sourceCode: packageJsonContentStr,
|
||||||
|
filePath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
|
||||||
|
|
||||||
|
await npmInstall({
|
||||||
|
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||||
|
});
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
136
src/bin/sync-extensions/managedGitignoreFile.ts
Normal file
136
src/bin/sync-extensions/managedGitignoreFile.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import {
|
||||||
|
join as pathJoin,
|
||||||
|
sep as pathSep,
|
||||||
|
dirname as pathDirname,
|
||||||
|
relative as pathRelative
|
||||||
|
} from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import type { ExtensionModuleMeta } from "./extensionModuleMeta";
|
||||||
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
themeSrcDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
const DELIMITER_START = `# === Owned files start ===`;
|
||||||
|
const DELIMITER_END = `# === Owned files end =====`;
|
||||||
|
|
||||||
|
export async function writeManagedGitignoreFile(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
extensionModuleMetas: ExtensionModuleMeta[];
|
||||||
|
ownedFilesRelativePaths: string[];
|
||||||
|
}): Promise<void> {
|
||||||
|
const { buildContext, extensionModuleMetas, ownedFilesRelativePaths } = params;
|
||||||
|
|
||||||
|
if (extensionModuleMetas.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
||||||
|
|
||||||
|
const content_new = Buffer.from(
|
||||||
|
[
|
||||||
|
`# This file is managed by Keycloakify, do not edit it manually.`,
|
||||||
|
``,
|
||||||
|
DELIMITER_START,
|
||||||
|
...ownedFilesRelativePaths
|
||||||
|
.map(fileRelativePath => fileRelativePath.split(pathSep).join("/"))
|
||||||
|
.map(line => `# ${line}`),
|
||||||
|
DELIMITER_END,
|
||||||
|
``,
|
||||||
|
...extensionModuleMetas
|
||||||
|
.map(extensionModuleMeta => [
|
||||||
|
`# === ${extensionModuleMeta.moduleName} v${extensionModuleMeta.version} ===`,
|
||||||
|
...extensionModuleMeta.files
|
||||||
|
.map(({ fileRelativePath }) => fileRelativePath)
|
||||||
|
.filter(
|
||||||
|
fileRelativePath =>
|
||||||
|
!ownedFilesRelativePaths.includes(fileRelativePath)
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
fileRelativePath =>
|
||||||
|
`/${fileRelativePath.split(pathSep).join("/").replace(/^\.\//, "")}`
|
||||||
|
),
|
||||||
|
|
||||||
|
``
|
||||||
|
])
|
||||||
|
.flat()
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const content_current = await (async () => {
|
||||||
|
if (!(await existsAsync(filePath))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fsPr.readFile(filePath);
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (content_current !== undefined && content_current.equals(content_new)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
create_dir: {
|
||||||
|
const dirPath = pathDirname(filePath);
|
||||||
|
|
||||||
|
if (await existsAsync(dirPath)) {
|
||||||
|
break create_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.mkdir(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.writeFile(filePath, content_new);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readManagedGitignoreFile(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<{
|
||||||
|
ownedFilesRelativePaths: string[];
|
||||||
|
}> {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
||||||
|
|
||||||
|
if (!(await existsAsync(filePath))) {
|
||||||
|
return { ownedFilesRelativePaths: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
|
||||||
|
|
||||||
|
const payload = (() => {
|
||||||
|
const index_start = contentStr.indexOf(DELIMITER_START);
|
||||||
|
const index_end = contentStr.indexOf(DELIMITER_END);
|
||||||
|
|
||||||
|
if (index_start === -1 || index_end === -1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentStr.slice(index_start + DELIMITER_START.length, index_end).trim();
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (payload === undefined) {
|
||||||
|
return { ownedFilesRelativePaths: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownedFilesRelativePaths = payload
|
||||||
|
.split("\n")
|
||||||
|
.map(line => line.trim())
|
||||||
|
.map(line => line.replace(/^# /, ""))
|
||||||
|
.filter(line => line !== "")
|
||||||
|
.map(line =>
|
||||||
|
getAbsoluteAndInOsFormatPath({
|
||||||
|
cwd: buildContext.themeSrcDirPath,
|
||||||
|
pathIsh: line
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath));
|
||||||
|
|
||||||
|
return { ownedFilesRelativePaths };
|
||||||
|
}
|
89
src/bin/sync-extensions/sync-extension.ts
Normal file
89
src/bin/sync-extensions/sync-extension.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import { getExtensionModuleMetas, computeHash } from "./extensionModuleMeta";
|
||||||
|
import { installExtensionModulesPeerDependencies } from "./installExtensionModulesPeerDependencies";
|
||||||
|
import {
|
||||||
|
readManagedGitignoreFile,
|
||||||
|
writeManagedGitignoreFile
|
||||||
|
} from "./managedGitignoreFile";
|
||||||
|
import { dirname as pathDirname } from "path";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { getIsKnownByGit } from "../tools/isKnownByGit";
|
||||||
|
import { untrackFromGit } from "../tools/untrackFromGit";
|
||||||
|
|
||||||
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
|
||||||
|
|
||||||
|
await installExtensionModulesPeerDependencies({
|
||||||
|
buildContext,
|
||||||
|
extensionModuleMetas
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ownedFilesRelativePaths } = await readManagedGitignoreFile({
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeManagedGitignoreFile({
|
||||||
|
buildContext,
|
||||||
|
ownedFilesRelativePaths,
|
||||||
|
extensionModuleMetas
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
extensionModuleMetas
|
||||||
|
.map(extensionModuleMeta =>
|
||||||
|
Promise.all(
|
||||||
|
extensionModuleMeta.files.map(
|
||||||
|
async ({ fileRelativePath, copyableFilePath, hash }) => {
|
||||||
|
if (ownedFilesRelativePaths.includes(fileRelativePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const destFilePath = pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
fileRelativePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const doesFileExist = await existsAsync(destFilePath);
|
||||||
|
|
||||||
|
skip_condition: {
|
||||||
|
if (!doesFileExist) {
|
||||||
|
break skip_condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const destFileHash = computeHash(
|
||||||
|
await fsPr.readFile(destFilePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (destFileHash !== hash) {
|
||||||
|
break skip_condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await getIsKnownByGit({ filePath: destFilePath })) {
|
||||||
|
await untrackFromGit({
|
||||||
|
filePath: destFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const dirName = pathDirname(destFilePath);
|
||||||
|
|
||||||
|
if (!(await existsAsync(dirName))) {
|
||||||
|
await fsPr.mkdir(dirName, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.copyFile(copyableFilePath, destFilePath);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
);
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
type PropertiesThatCanBeUndefined<T extends Record<string, unknown>> = {
|
|
||||||
[Key in keyof T]: undefined extends T[Key] ? Key : never;
|
|
||||||
}[keyof T];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OptionalIfCanBeUndefined<{ p1: string | undefined; p2: string; }>
|
|
||||||
* is
|
|
||||||
* { p1?: string | undefined; p2: string }
|
|
||||||
*/
|
|
||||||
export type OptionalIfCanBeUndefined<T extends Record<string, unknown>> = {
|
|
||||||
[K in PropertiesThatCanBeUndefined<T>]?: T[K];
|
|
||||||
} & { [K in Exclude<keyof T, PropertiesThatCanBeUndefined<T>>]: T[K] };
|
|
99
src/bin/tools/Stringifyable.ts
Normal file
99
src/bin/tools/Stringifyable.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { same } from "evt/tools/inDepth/same";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
|
export type Stringifyable =
|
||||||
|
| StringifyableAtomic
|
||||||
|
| StringifyableObject
|
||||||
|
| StringifyableArray;
|
||||||
|
|
||||||
|
export type StringifyableAtomic = string | number | boolean | null;
|
||||||
|
|
||||||
|
// NOTE: Use Record<string, Stringifyable>
|
||||||
|
interface StringifyableObject {
|
||||||
|
[key: string]: Stringifyable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Use Stringifyable[]
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
interface StringifyableArray extends Array<Stringifyable> {}
|
||||||
|
|
||||||
|
export const zStringifyableAtomic = (() => {
|
||||||
|
type TargetType = StringifyableAtomic;
|
||||||
|
|
||||||
|
const zTargetType = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const zStringifyable: z.ZodType<Stringifyable> = z
|
||||||
|
.any()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
const isStringifyable = same(JSON.parse(JSON.stringify(val)), val);
|
||||||
|
if (!isStringifyable) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Not stringifyable"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getIsAtomic(
|
||||||
|
stringifyable: Stringifyable
|
||||||
|
): stringifyable is StringifyableAtomic {
|
||||||
|
return (
|
||||||
|
["string", "number", "boolean"].includes(typeof stringifyable) ||
|
||||||
|
stringifyable === null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { getValueAtPath } = (() => {
|
||||||
|
function getValueAtPath_rec(
|
||||||
|
stringifyable: Stringifyable,
|
||||||
|
path: (string | number)[]
|
||||||
|
): Stringifyable | undefined {
|
||||||
|
if (path.length === 0) {
|
||||||
|
return stringifyable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsAtomic(stringifyable)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [first, ...rest] = path;
|
||||||
|
|
||||||
|
let dereferenced: Stringifyable | undefined;
|
||||||
|
|
||||||
|
if (stringifyable instanceof Array) {
|
||||||
|
if (typeof first !== "number") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
dereferenced = stringifyable[first];
|
||||||
|
} else {
|
||||||
|
if (typeof first !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
dereferenced = stringifyable[first];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dereferenced === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getValueAtPath_rec(dereferenced, rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValueAtPath(
|
||||||
|
stringifyableObjectOrArray: Record<string, Stringifyable> | Stringifyable[],
|
||||||
|
path: (string | number)[]
|
||||||
|
): Stringifyable | undefined {
|
||||||
|
return getValueAtPath_rec(stringifyableObjectOrArray, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getValueAtPath };
|
||||||
|
})();
|
164
src/bin/tools/canonicalStringify.ts
Normal file
164
src/bin/tools/canonicalStringify.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { getIsAtomic, getValueAtPath, type Stringifyable } from "./Stringifyable";
|
||||||
|
|
||||||
|
export function canonicalStringify(params: {
|
||||||
|
data: Record<string, Stringifyable> | Stringifyable[];
|
||||||
|
referenceData: Record<string, Stringifyable> | Stringifyable[];
|
||||||
|
}): string {
|
||||||
|
const { data, referenceData } = params;
|
||||||
|
|
||||||
|
return JSON.stringify(
|
||||||
|
makeDeterministicCopy({
|
||||||
|
path: [],
|
||||||
|
data,
|
||||||
|
getCanonicalKeys: path => {
|
||||||
|
const referenceValue = (() => {
|
||||||
|
const path_patched: (string | number)[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
let value_i = getValueAtPath(referenceData, [
|
||||||
|
...path_patched,
|
||||||
|
path[i]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (value_i !== undefined) {
|
||||||
|
path_patched.push(path[i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof path[i] !== "number") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
value_i = getValueAtPath(referenceData, [...path_patched, 0]);
|
||||||
|
|
||||||
|
if (value_i !== undefined) {
|
||||||
|
path_patched.push(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getValueAtPath(referenceData, path_patched);
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (referenceValue === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsAtomic(referenceValue)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referenceValue instanceof Array) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(referenceValue);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDeterministicCopy(params: {
|
||||||
|
path: (string | number)[];
|
||||||
|
data: Stringifyable;
|
||||||
|
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
|
||||||
|
}): Stringifyable {
|
||||||
|
const { path, data, getCanonicalKeys } = params;
|
||||||
|
|
||||||
|
if (getIsAtomic(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data instanceof Array) {
|
||||||
|
return makeDeterministicCopy_array({
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
getCanonicalKeys
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeDeterministicCopy_record({
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
getCanonicalKeys
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDeterministicCopy_record(params: {
|
||||||
|
path: (string | number)[];
|
||||||
|
data: Record<string, Stringifyable>;
|
||||||
|
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
|
||||||
|
}): Record<string, Stringifyable> {
|
||||||
|
const { path, data, getCanonicalKeys } = params;
|
||||||
|
|
||||||
|
const keysOfAtomicValues: string[] = [];
|
||||||
|
const keysOfNonAtomicValues: string[] = [];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
if (getIsAtomic(value)) {
|
||||||
|
keysOfAtomicValues.push(key);
|
||||||
|
} else {
|
||||||
|
keysOfNonAtomicValues.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keysOfAtomicValues.sort();
|
||||||
|
keysOfNonAtomicValues.sort();
|
||||||
|
|
||||||
|
const keys = [...keysOfAtomicValues, ...keysOfNonAtomicValues];
|
||||||
|
|
||||||
|
reorder_according_to_canonical: {
|
||||||
|
const canonicalKeys = getCanonicalKeys(path);
|
||||||
|
|
||||||
|
if (canonicalKeys === undefined) {
|
||||||
|
break reorder_according_to_canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys_toPrepend: string[] = [];
|
||||||
|
|
||||||
|
for (const key of canonicalKeys) {
|
||||||
|
const indexOfKey = keys.indexOf(key);
|
||||||
|
|
||||||
|
if (indexOfKey === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.splice(indexOfKey, 1);
|
||||||
|
keys_toPrepend.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.unshift(...keys_toPrepend);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, Stringifyable> = {};
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
result[key] = makeDeterministicCopy({
|
||||||
|
path: [...path, key],
|
||||||
|
data: data[key],
|
||||||
|
getCanonicalKeys
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDeterministicCopy_array(params: {
|
||||||
|
path: (string | number)[];
|
||||||
|
data: Stringifyable[];
|
||||||
|
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
|
||||||
|
}): Stringifyable[] {
|
||||||
|
const { path, data, getCanonicalKeys } = params;
|
||||||
|
|
||||||
|
return [...data].map((entry, i) =>
|
||||||
|
makeDeterministicCopy({
|
||||||
|
path: [...path, i],
|
||||||
|
data: entry,
|
||||||
|
getCanonicalKeys
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
51
src/bin/tools/crawlAsync.ts
Normal file
51
src/bin/tools/crawlAsync.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
|
||||||
|
/** List all files in a given directory return paths relative to the dir_path */
|
||||||
|
export async function crawlAsync(params: {
|
||||||
|
dirPath: string;
|
||||||
|
returnedPathsType: "absolute" | "relative to dirPath";
|
||||||
|
onFileFound: (filePath: string) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const { dirPath, returnedPathsType, onFileFound } = params;
|
||||||
|
|
||||||
|
await crawlAsyncRec({
|
||||||
|
dirPath,
|
||||||
|
onFileFound: async ({ filePath }) => {
|
||||||
|
switch (returnedPathsType) {
|
||||||
|
case "absolute":
|
||||||
|
await onFileFound(filePath);
|
||||||
|
return;
|
||||||
|
case "relative to dirPath":
|
||||||
|
await onFileFound(pathRelative(dirPath, filePath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert<Equals<typeof returnedPathsType, never>>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function crawlAsyncRec(params: {
|
||||||
|
dirPath: string;
|
||||||
|
onFileFound: (params: { filePath: string }) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const { dirPath, onFileFound } = params;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
(await fsPr.readdir(dirPath)).map(async basename => {
|
||||||
|
const fileOrDirPath = pathJoin(dirPath, basename);
|
||||||
|
|
||||||
|
const isDirectory = await fsPr
|
||||||
|
.lstat(fileOrDirPath)
|
||||||
|
.then(stat => stat.isDirectory());
|
||||||
|
|
||||||
|
if (isDirectory) {
|
||||||
|
await crawlAsyncRec({ dirPath: fileOrDirPath, onFileFound });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await onFileFound({ filePath: fileOrDirPath });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
@ -1,73 +0,0 @@
|
|||||||
import { Readable } from "stream";
|
|
||||||
|
|
||||||
const crc32tab = [
|
|
||||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535,
|
|
||||||
0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd,
|
|
||||||
0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d,
|
|
||||||
0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
|
|
||||||
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
|
|
||||||
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
|
|
||||||
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac,
|
|
||||||
0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
|
||||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
|
|
||||||
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
|
|
||||||
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb,
|
|
||||||
0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
|
||||||
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea,
|
|
||||||
0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce,
|
|
||||||
0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
|
|
||||||
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
|
||||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409,
|
|
||||||
0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
|
|
||||||
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739,
|
|
||||||
0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
|
|
||||||
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268,
|
|
||||||
0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0,
|
|
||||||
0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8,
|
|
||||||
0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
|
||||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
|
|
||||||
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,
|
|
||||||
0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
|
|
||||||
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
|
|
||||||
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae,
|
|
||||||
0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
|
|
||||||
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6,
|
|
||||||
0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
|
||||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d,
|
|
||||||
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
|
|
||||||
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
|
|
||||||
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
|
||||||
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param input either a byte stream, a string or a buffer, you want to have the checksum for
|
|
||||||
* @returns a promise for a checksum (uint32)
|
|
||||||
*/
|
|
||||||
export function crc32(input: Readable | String | Buffer): Promise<number> {
|
|
||||||
if (typeof input === "string") {
|
|
||||||
let crc = ~0;
|
|
||||||
for (let i = 0; i < input.length; i++)
|
|
||||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
|
|
||||||
return Promise.resolve((crc ^ -1) >>> 0);
|
|
||||||
} else if (input instanceof Buffer) {
|
|
||||||
let crc = ~0;
|
|
||||||
for (let i = 0; i < input.length; i++)
|
|
||||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
|
|
||||||
return Promise.resolve((crc ^ -1) >>> 0);
|
|
||||||
} else if (input instanceof Readable) {
|
|
||||||
return new Promise<number>((resolve, reject) => {
|
|
||||||
let crc = ~0;
|
|
||||||
input.setMaxListeners(Infinity);
|
|
||||||
input.on("end", () => resolve((crc ^ -1) >>> 0));
|
|
||||||
input.on("error", e => reject(e));
|
|
||||||
input.on("data", (chunk: Buffer) => {
|
|
||||||
for (let i = 0; i < chunk.length; i++)
|
|
||||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error("Unsupported input " + typeof input);
|
|
||||||
}
|
|
||||||
}
|
|
90
src/bin/tools/createObjectThatThrowsIfAccessed.ts
Normal file
90
src/bin/tools/createObjectThatThrowsIfAccessed.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
const keyIsTrapped = "isTrapped_zSskDe9d";
|
||||||
|
|
||||||
|
export class AccessError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createObjectThatThrowsIfAccessed<T extends object>(params?: {
|
||||||
|
debugMessage?: string;
|
||||||
|
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
|
||||||
|
}): T {
|
||||||
|
const { debugMessage = "", isPropertyWhitelisted = () => false } = params ?? {};
|
||||||
|
|
||||||
|
const get: NonNullable<ProxyHandler<T>["get"]> = (...args) => {
|
||||||
|
const [, prop] = args;
|
||||||
|
|
||||||
|
if (isPropertyWhitelisted(prop)) {
|
||||||
|
return Reflect.get(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === keyIsTrapped) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AccessError(`Cannot access ${String(prop)} yet ${debugMessage}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const trappedObject = new Proxy<T>({} as any, {
|
||||||
|
get,
|
||||||
|
set: get
|
||||||
|
});
|
||||||
|
|
||||||
|
return trappedObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createObjectThatThrowsIfAccessedFactory(params: {
|
||||||
|
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
|
||||||
|
}) {
|
||||||
|
const { isPropertyWhitelisted } = params;
|
||||||
|
|
||||||
|
return {
|
||||||
|
createObjectThatThrowsIfAccessed: <T extends object>(params?: {
|
||||||
|
debugMessage?: string;
|
||||||
|
}) => {
|
||||||
|
const { debugMessage } = params ?? {};
|
||||||
|
|
||||||
|
return createObjectThatThrowsIfAccessed<T>({
|
||||||
|
debugMessage,
|
||||||
|
isPropertyWhitelisted
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObjectThatThrowIfAccessed(obj: object) {
|
||||||
|
return (obj as any)[keyIsTrapped] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const THROW_IF_ACCESSED = {
|
||||||
|
__brand: "THROW_IF_ACCESSED"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createObjectWithSomePropertiesThatThrowIfAccessed<
|
||||||
|
T extends Record<string, unknown>
|
||||||
|
>(obj: { [K in keyof T]: T[K] | typeof THROW_IF_ACCESSED }, debugMessage?: string): T {
|
||||||
|
return Object.defineProperties(
|
||||||
|
obj,
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(obj)
|
||||||
|
.filter(([, value]) => value === THROW_IF_ACCESSED)
|
||||||
|
.map(([key]) => {
|
||||||
|
const getAndSet = () => {
|
||||||
|
throw new AccessError(
|
||||||
|
`Cannot access ${key} yet ${debugMessage ?? ""}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pd = {
|
||||||
|
get: getAndSet,
|
||||||
|
set: getAndSet,
|
||||||
|
enumerable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
return [key, pd];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
) as any;
|
||||||
|
}
|
@ -1,61 +0,0 @@
|
|||||||
import { PassThrough, Readable, TransformCallback, Writable } from "stream";
|
|
||||||
import { pipeline } from "stream/promises";
|
|
||||||
import { deflateRaw as deflateRawCb, createDeflateRaw } from "zlib";
|
|
||||||
import { promisify } from "util";
|
|
||||||
|
|
||||||
import { crc32 } from "./crc32";
|
|
||||||
import tee from "./tee";
|
|
||||||
|
|
||||||
const deflateRaw = promisify(deflateRawCb);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A stream transformer that records the number of bytes
|
|
||||||
* passed in its `size` property.
|
|
||||||
*/
|
|
||||||
class ByteCounter extends PassThrough {
|
|
||||||
size: number = 0;
|
|
||||||
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) {
|
|
||||||
if ("length" in chunk) this.size += chunk.length;
|
|
||||||
super._transform(chunk, encoding, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param data buffer containing the data to be compressed
|
|
||||||
* @returns a buffer containing the compressed/deflated data and the crc32 checksum
|
|
||||||
* of the source data
|
|
||||||
*/
|
|
||||||
export async function deflateBuffer(data: Buffer) {
|
|
||||||
const [deflated, checksum] = await Promise.all([deflateRaw(data), crc32(data)]);
|
|
||||||
return { deflated, crc32: checksum };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param input a byte stream, containing data to be compressed
|
|
||||||
* @param sink a method that will accept chunks of compressed data; We don't pass
|
|
||||||
* a writable here, since we don't want the writablestream to be closed after
|
|
||||||
* a single file
|
|
||||||
* @returns a promise, which will resolve with the crc32 checksum and the
|
|
||||||
* compressed size
|
|
||||||
*/
|
|
||||||
export async function deflateStream(input: Readable, sink: (chunk: Buffer) => void) {
|
|
||||||
const deflateWriter = new Writable({
|
|
||||||
write(chunk, _, callback) {
|
|
||||||
sink(chunk);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// tee the input stream, so we can compress and calc crc32 in parallel
|
|
||||||
const [rs1, rs2] = tee(input);
|
|
||||||
const byteCounter = new ByteCounter();
|
|
||||||
const [_, crc] = await Promise.all([
|
|
||||||
// pipe input into zip compressor, count the bytes
|
|
||||||
// returned and pass compressed data to the sink
|
|
||||||
pipeline(rs1, createDeflateRaw(), byteCounter, deflateWriter),
|
|
||||||
// calc checksum
|
|
||||||
crc32(rs2)
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { crc32: crc, compressedSize: byteCounter.size };
|
|
||||||
}
|
|
@ -1,16 +1,18 @@
|
|||||||
import { type FetchOptions } from "make-fetch-happen";
|
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { exclude } from "tsafe/exclude";
|
import { exclude } from "tsafe/exclude";
|
||||||
|
|
||||||
export type ProxyFetchOptions = Pick<
|
export type FetchOptionsLike = {
|
||||||
FetchOptions,
|
proxy: string | undefined;
|
||||||
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
|
noProxy: string | string[];
|
||||||
>;
|
strictSSL: boolean;
|
||||||
|
cert: string | string[] | undefined;
|
||||||
|
ca: string[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export function getProxyFetchOptions(params: {
|
export function getProxyFetchOptions(params: {
|
||||||
npmConfigGetCwd: string;
|
npmConfigGetCwd: string;
|
||||||
}): ProxyFetchOptions {
|
}): FetchOptionsLike {
|
||||||
const { npmConfigGetCwd } = params;
|
const { npmConfigGetCwd } = params;
|
||||||
|
|
||||||
const cfg = (() => {
|
const cfg = (() => {
|
||||||
|
@ -14,6 +14,8 @@ export function getAbsoluteAndInOsFormatPath(params: {
|
|||||||
|
|
||||||
let pathOut = pathIsh;
|
let pathOut = pathIsh;
|
||||||
|
|
||||||
|
pathOut = pathOut.replace(/^['"]/, "").replace(/['"]$/, "");
|
||||||
|
|
||||||
pathOut = pathOut.replace(/\//g, pathSep);
|
pathOut = pathOut.replace(/\//g, pathSep);
|
||||||
|
|
||||||
if (pathOut.startsWith("~")) {
|
if (pathOut.startsWith("~")) {
|
||||||
|
53
src/bin/tools/getInstalledModuleDirPath.ts
Normal file
53
src/bin/tools/getInstalledModuleDirPath.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { existsAsync } from "./fs.existsAsync";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { getIsRootPath } from "../tools/isRootPath";
|
||||||
|
|
||||||
|
export async function getInstalledModuleDirPath(params: {
|
||||||
|
moduleName: string;
|
||||||
|
packageJsonDirPath: string;
|
||||||
|
}) {
|
||||||
|
const { moduleName, packageJsonDirPath } = params;
|
||||||
|
|
||||||
|
{
|
||||||
|
let dirPath = packageJsonDirPath;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const dirPath_candidate = pathJoin(
|
||||||
|
dirPath,
|
||||||
|
"node_modules",
|
||||||
|
...moduleName.split("/")
|
||||||
|
);
|
||||||
|
|
||||||
|
let doesExist: boolean;
|
||||||
|
|
||||||
|
try {
|
||||||
|
doesExist = await existsAsync(dirPath_candidate);
|
||||||
|
} catch {
|
||||||
|
doesExist = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doesExist) {
|
||||||
|
return dirPath_candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsRootPath(dirPath)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirPath = pathJoin(dirPath, "..");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirPath = child_process
|
||||||
|
.execSync(`npm list ${moduleName}`, {
|
||||||
|
cwd: packageJsonDirPath
|
||||||
|
})
|
||||||
|
.toString("utf8")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
assert(dirPath !== "");
|
||||||
|
|
||||||
|
return dirPath;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user